From 8b4d12e3ef42b4fc58bb7359ae189f0a88304a9d Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 8 Jan 2026 09:18:48 +0100 Subject: [PATCH 01/30] feat(angular): wip add table context and hook helpers --- .../.devcontainer/devcontainer.json | 4 + .../angular/composable-tables/.editorconfig | 16 + examples/angular/composable-tables/.gitignore | 42 ++ examples/angular/composable-tables/README.md | 27 ++ .../angular/composable-tables/angular.json | 107 +++++ .../angular/composable-tables/package.json | 34 ++ .../src/app/app.component.html | 76 ++++ .../src/app/app.component.ts | 77 ++++ .../composable-tables/src/app/app.config.ts | 7 + .../composable-tables/src/app/app.routes.ts | 3 + .../src/app/components/header-components.ts | 24 ++ .../src/app/components/table-components.ts | 29 ++ .../composable-tables/src/app/makeData.ts | 77 ++++ .../composable-tables/src/app/table.ts | 94 +++++ .../composable-tables/src/assets/.gitkeep | 0 .../angular/composable-tables/src/favicon.ico | Bin 0 -> 15086 bytes .../angular/composable-tables/src/index.html | 14 + .../angular/composable-tables/src/main.ts | 5 + .../angular/composable-tables/src/styles.scss | 32 ++ .../composable-tables/tsconfig.app.json | 10 + .../angular/composable-tables/tsconfig.json | 31 ++ .../composable-tables/tsconfig.spec.json | 9 + packages/angular-table/src/context/cell.ts | 31 ++ .../src/context/createTableContexts.ts | 383 ++++++++++++++++++ .../angular-table/src/context/flexRender.ts | 1 + packages/angular-table/src/context/header.ts | 33 ++ packages/angular-table/src/context/table.ts | 30 ++ packages/angular-table/src/flex-render.ts | 3 +- packages/angular-table/src/index.ts | 5 + .../src/lazySignalInitializer.ts | 17 +- pnpm-lock.yaml | 55 +++ 31 files changed, 1274 insertions(+), 2 deletions(-) create mode 100644 examples/angular/composable-tables/.devcontainer/devcontainer.json create mode 100644 examples/angular/composable-tables/.editorconfig create mode 100644 examples/angular/composable-tables/.gitignore create mode 100644 examples/angular/composable-tables/README.md create mode 100644 examples/angular/composable-tables/angular.json create mode 100644 examples/angular/composable-tables/package.json create mode 100644 examples/angular/composable-tables/src/app/app.component.html create mode 100644 examples/angular/composable-tables/src/app/app.component.ts create mode 100644 examples/angular/composable-tables/src/app/app.config.ts create mode 100644 examples/angular/composable-tables/src/app/app.routes.ts create mode 100644 examples/angular/composable-tables/src/app/components/header-components.ts create mode 100644 examples/angular/composable-tables/src/app/components/table-components.ts create mode 100644 examples/angular/composable-tables/src/app/makeData.ts create mode 100644 examples/angular/composable-tables/src/app/table.ts create mode 100644 examples/angular/composable-tables/src/assets/.gitkeep create mode 100644 examples/angular/composable-tables/src/favicon.ico create mode 100644 examples/angular/composable-tables/src/index.html create mode 100644 examples/angular/composable-tables/src/main.ts create mode 100644 examples/angular/composable-tables/src/styles.scss create mode 100644 examples/angular/composable-tables/tsconfig.app.json create mode 100644 examples/angular/composable-tables/tsconfig.json create mode 100644 examples/angular/composable-tables/tsconfig.spec.json create mode 100644 packages/angular-table/src/context/cell.ts create mode 100644 packages/angular-table/src/context/createTableContexts.ts create mode 100644 packages/angular-table/src/context/flexRender.ts create mode 100644 packages/angular-table/src/context/header.ts create mode 100644 packages/angular-table/src/context/table.ts diff --git a/examples/angular/composable-tables/.devcontainer/devcontainer.json b/examples/angular/composable-tables/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..36f47d8762 --- /dev/null +++ b/examples/angular/composable-tables/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/composable-tables/.editorconfig b/examples/angular/composable-tables/.editorconfig new file mode 100644 index 0000000000..59d9a3a3e7 --- /dev/null +++ b/examples/angular/composable-tables/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/angular/composable-tables/.gitignore b/examples/angular/composable-tables/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/examples/angular/composable-tables/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/composable-tables/README.md b/examples/angular/composable-tables/README.md new file mode 100644 index 0000000000..5da97a87d1 --- /dev/null +++ b/examples/angular/composable-tables/README.md @@ -0,0 +1,27 @@ +# Basic + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/composable-tables/angular.json b/examples/angular/composable-tables/angular.json new file mode 100644 index 0000000000..7d88b1324b --- /dev/null +++ b/examples/angular/composable-tables/angular.json @@ -0,0 +1,107 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "composable": { + "cli": { + "cache": { + "enabled": false + } + }, + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true, + "style": "scss" + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/composable", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "composable:build:production" + }, + "development": { + "buildTarget": "composable:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "composable:build" + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/examples/angular/composable-tables/package.json b/examples/angular/composable-tables/package.json new file mode 100644 index 0000000000..3bc4d07a85 --- /dev/null +++ b/examples/angular/composable-tables/package.json @@ -0,0 +1,34 @@ +{ + "name": "tanstack-table-example-angular-composable-tables", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "lint": "eslint ./src" + }, + "private": true, + "dependencies": { + "@angular/common": "^21.0.6", + "@angular/compiler": "^21.0.6", + "@angular/core": "^21.0.6", + "@angular/forms": "^21.0.6", + "@angular/platform-browser": "^21.0.6", + "@angular/platform-browser-dynamic": "^21.0.6", + "@angular/router": "^21.0.6", + "@tanstack/angular-table": "^9.0.0-alpha.10", + "rxjs": "~7.8.2", + "zone.js": "~0.16.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.6", + "@types/jasmine": "~5.1.13", + "jasmine-core": "~5.13.0", + "tslib": "^2.8.1", + "typescript": "5.9.3" + } +} diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html new file mode 100644 index 0000000000..79fb40bfa3 --- /dev/null +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -0,0 +1,76 @@ +
+
+ + + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + @if (!header.isPlaceholder) { + + } + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (c of row.getAllCells(); track c.id) { + @let cell = table.AppCell(c); + + + } + + } + + + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (footer of footerGroup.headers; track footer.id) { + + } + + } + +
+ + +
+
+
+ +
+
+
+ + {{ footer }} + +
+ +
+ +
+ +
diff --git a/examples/angular/composable-tables/src/app/app.component.ts b/examples/angular/composable-tables/src/app/app.component.ts new file mode 100644 index 0000000000..f9d6b8d1bd --- /dev/null +++ b/examples/angular/composable-tables/src/app/app.component.ts @@ -0,0 +1,77 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + TanStackTable, + TanStackTableHeader, +} from '@tanstack/angular-table' +import { NgComponentOutlet } from '@angular/common' +import { createAppColumnHelper, injectAppTable } from './table' +import { makeData } from './makeData' +import type { Person, Product } from './makeData' + +// Create column helpers with TFeatures already bound - only need TData! +const personColumnHelper = createAppColumnHelper() +const productColumnHelper = createAppColumnHelper() + +@Component({ + selector: 'app-root', + imports: [FlexRender, TanStackTableHeader, NgComponentOutlet, TanStackTable], + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + readonly data = signal(makeData(5000)) + + readonly columns = personColumnHelper.columns([ + personColumnHelper.accessor('firstName', { + header: 'First Name', + footer: (props) => props.column.id, + // cell: ({ cell }) => , + }), + personColumnHelper.accessor('lastName', { + header: 'Last Name', + footer: (props) => props.column.id, + // cell: ({ cell }) => , + }), + personColumnHelper.accessor('age', { + header: 'Age', + footer: (props) => props.column.id, + // cell: ({ cell }) => , + }), + personColumnHelper.accessor('visits', { + header: 'Visits', + footer: (props) => props.column.id, + // cell: ({ cell }) => , + }), + personColumnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + // cell: ({ cell }) => , + }), + personColumnHelper.accessor('progress', { + header: 'Progress', + footer: (props) => props.column.id, + // cell: ({ cell }) => , + }), + personColumnHelper.display({ + id: 'actions', + header: 'Actions', + // cell: ({ cell }) => , + }), + ]) + + table = injectAppTable(() => ({ + columns: this.columns, + data: this.data(), + debugTable: true, + // more table options + })) + + onRefresh = () => { + this.data.set([...makeData(5000)]) + } + + constructor() {} + + rerender() {} +} diff --git a/examples/angular/composable-tables/src/app/app.config.ts b/examples/angular/composable-tables/src/app/app.config.ts new file mode 100644 index 0000000000..828af23571 --- /dev/null +++ b/examples/angular/composable-tables/src/app/app.config.ts @@ -0,0 +1,7 @@ +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes)], +} diff --git a/examples/angular/composable-tables/src/app/app.routes.ts b/examples/angular/composable-tables/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/composable-tables/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/composable-tables/src/app/components/header-components.ts b/examples/angular/composable-tables/src/app/components/header-components.ts new file mode 100644 index 0000000000..f52f3210dc --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/header-components.ts @@ -0,0 +1,24 @@ +// export function SortIndicator() { +// const header = useHeaderContext() +// const sorted = header.column.getIsSorted() +// +// if (!sorted) return null +// +// return ( +// {sorted === 'asc' ? '🔼' : '🔽'} +// ) +// } + +import { Component } from '@angular/core' +import { injectTableHeaderContext } from '@tanstack/angular-table' + +@Component({ + selector: 'app-sort-indicator', + host: { + class: 'sort-indicator', + }, + template: ` {{ sorted === 'asc' ? '🔼' : '🔽' }} `, +}) +export class SortIndicator { + readonly context = injectTableHeaderContext() +} diff --git a/examples/angular/composable-tables/src/app/components/table-components.ts b/examples/angular/composable-tables/src/app/components/table-components.ts new file mode 100644 index 0000000000..6d752982c2 --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/table-components.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { injectTableContext } from '../table' + +@Component({ + template: ` +
+

{{ title() }}

+ + + + @if (onRefresh(); as onRefresh) { + + } +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableToolbar { + readonly title = input.required() + readonly onRefresh = input<() => void>() + + readonly context = injectTableContext() + + constructor() { + this.context.table().resetColumnFilters() + } +} diff --git a/examples/angular/composable-tables/src/app/makeData.ts b/examples/angular/composable-tables/src/app/makeData.ts new file mode 100644 index 0000000000..17dec1c6de --- /dev/null +++ b/examples/angular/composable-tables/src/app/makeData.ts @@ -0,0 +1,77 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +export type Product = { + id: string + name: string + category: 'electronics' | 'clothing' | 'food' | 'books' + price: number + stock: number + rating: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +const newProduct = (): Product => { + return { + id: faker.string.uuid(), + name: faker.commerce.productName(), + category: faker.helpers.shuffle([ + 'electronics', + 'clothing', + 'food', + 'books', + ])[0], + price: parseFloat(faker.commerce.price({ min: 5, max: 500 })), + stock: faker.number.int({ min: 0, max: 200 }), + rating: faker.number.int({ min: 0, max: 100 }), + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} + +export function makeProductData(count: number): Array { + return range(count).map(() => newProduct()) +} diff --git a/examples/angular/composable-tables/src/app/table.ts b/examples/angular/composable-tables/src/app/table.ts new file mode 100644 index 0000000000..33f411d8cb --- /dev/null +++ b/examples/angular/composable-tables/src/app/table.ts @@ -0,0 +1,94 @@ +/** + * Custom table hook setup using createTableHook + * + * This file creates a custom useAppTable hook with pre-bound components. + * Features, row models, and default options are defined once here and shared across all tables. + * Context hooks and a pre-bound createAppColumnHelper are also exported. + */ +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableContexts, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +// Import table-level components +import { TableToolbar } from './components/table-components' + +// Import table-level components +// import { +// PaginationControls, +// RowCount, +// TableToolbar, +// } from '../components/table-components' + +// Import cell-level components + +// Import header/footer-level components (both use useHeaderContext) + +/** + * Create the custom table hook with all pre-bound components. + * This exports: + * - createAppColumnHelper: Create column definitions with TFeatures already bound + * - useAppTable: Hook for creating tables with TFeatures baked in + * - useTableContext: Access table instance in tableComponents + * - useCellContext: Access cell instance in cellComponents + * - useHeaderContext: Access header instance in headerComponents + */ +export const { + createAppColumnHelper, + injectAppTable, + injectTableContext, + // useAppTable, + // useTableContext, + // useCellContext, + // useHeaderContext, +} = createTableContexts({ + // Features are set once here and shared across all tables + _features: tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, + }), + + // Row models are set once here + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + filteredRowModel: createFilteredRowModel(filterFns), + paginatedRowModel: createPaginatedRowModel(), + }, + + // set any default table options here too + getRowId: (row) => row.id, + + // Register table-level components (accessible via table.ComponentName) + tableComponents: { + // PaginationControls, + // RowCount, + TableToolbar, + }, + + // Register cell-level components (accessible via cell.ComponentName in AppCell) + cellComponents: { + // TextCell, + // NumberCell, + // StatusCell, + // ProgressCell, + // RowActionsCell, + // PriceCell, + // CategoryCell, + }, + + // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) + headerComponents: { + // SortIndicator, + // ColumnFilter, + // FooterColumnId, + // FooterSum, + }, +}) diff --git a/examples/angular/composable-tables/src/assets/.gitkeep b/examples/angular/composable-tables/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/angular/composable-tables/src/favicon.ico b/examples/angular/composable-tables/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/examples/angular/composable-tables/src/index.html b/examples/angular/composable-tables/src/index.html new file mode 100644 index 0000000000..a4bb987648 --- /dev/null +++ b/examples/angular/composable-tables/src/index.html @@ -0,0 +1,14 @@ + + + + + Basic + + + + + + + + + diff --git a/examples/angular/composable-tables/src/main.ts b/examples/angular/composable-tables/src/main.ts new file mode 100644 index 0000000000..c3d8f9af99 --- /dev/null +++ b/examples/angular/composable-tables/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/composable-tables/src/styles.scss b/examples/angular/composable-tables/src/styles.scss new file mode 100644 index 0000000000..cda3113f7d --- /dev/null +++ b/examples/angular/composable-tables/src/styles.scss @@ -0,0 +1,32 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +.pagination-actions { + margin: 10px; + display: flex; + gap: 10px; +} diff --git a/examples/angular/composable-tables/tsconfig.app.json b/examples/angular/composable-tables/tsconfig.app.json new file mode 100644 index 0000000000..84f1f992d2 --- /dev/null +++ b/examples/angular/composable-tables/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/composable-tables/tsconfig.json b/examples/angular/composable-tables/tsconfig.json new file mode 100644 index 0000000000..b58d3efc71 --- /dev/null +++ b/examples/angular/composable-tables/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "src", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/composable-tables/tsconfig.spec.json b/examples/angular/composable-tables/tsconfig.spec.json new file mode 100644 index 0000000000..47e3dd7551 --- /dev/null +++ b/examples/angular/composable-tables/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/packages/angular-table/src/context/cell.ts b/packages/angular-table/src/context/cell.ts new file mode 100644 index 0000000000..356ad17eaf --- /dev/null +++ b/packages/angular-table/src/context/cell.ts @@ -0,0 +1,31 @@ +import { Directive, inject, input } from '@angular/core' +import { Cell, CellData, RowData, TableFeatures } from '@tanstack/table-core' +import type { Signal } from '@angular/core' + +export interface TanStackTableCellContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +> { + cell: Signal> +} + +@Directive({ + selector: '[tanStackTableCell]', + exportAs: 'cell', +}) +export class TanStackTableCell< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +> implements TanStackTableCellContext { + readonly cell = input.required>() +} + +export function injectTableCellContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +>(): TanStackTableCellContext { + return inject(TanStackTableCell) +} diff --git a/packages/angular-table/src/context/createTableContexts.ts b/packages/angular-table/src/context/createTableContexts.ts new file mode 100644 index 0000000000..0bd001a0e7 --- /dev/null +++ b/packages/angular-table/src/context/createTableContexts.ts @@ -0,0 +1,383 @@ +import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' +import { injectTable } from '../injectTable' +import { injectTableHeaderContext as _injectTableHeaderContext } from './header' +import { injectTableContext as _injetTableContext } from './table' +import { injectTableCellContext as _injectTableCellContext } from './cell' +import type { AngularTable } from '../injectTable' +import type { + AccessorFn, + AccessorFnColumnDef, + AccessorKeyColumnDef, + Cell, + CellContext, + CellData, + Column, + ColumnDef, + DeepKeys, + DeepValue, + DisplayColumnDef, + GroupColumnDef, + Header, + IdentifiedColumnDef, + Row, + RowData, + Table, + TableFeatures, + TableOptions, + TableState, +} from '@tanstack/table-core' +import type { Type } from '@angular/core' +import type { FlexRenderContent } from '../flex-render' + +type RenderableComponent = + | Type + | (>(props: T) => FlexRenderContent) + +// ============================================================================= +// Enhanced Context Types with Pre-bound Components +// ============================================================================= + +/** + * Enhanced CellContext with pre-bound cell components. + * The `cell` property includes the registered cellComponents. + */ +export type AppCellContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, +> = { + cell: Cell & + TCellComponents & { FlexRender: () => unknown } + column: Column + getValue: CellContext['getValue'] + renderValue: CellContext['renderValue'] + row: Row + table: Table +} + +/** + * Enhanced HeaderContext with pre-bound header components. + * The `header` property includes the registered headerComponents. + */ +export type AppHeaderContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record, +> = { + column: Column + header: Header & + THeaderComponents & { FlexRender: () => unknown } + table: Table +} + +// ============================================================================= +// Enhanced Column Definition Types +// ============================================================================= + +/** + * Template type for column definitions that can be a string or a function. + */ +type AppColumnDefTemplate = + | string + | ((props: TProps) => any) + +/** + * Enhanced column definition base with pre-bound components in cell/header/footer contexts. + */ +type AppColumnDefBase< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + IdentifiedColumnDef, + 'cell' | 'header' | 'footer' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > +} + +/** + * Enhanced display column definition with pre-bound components. + */ +type AppDisplayColumnDef< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + DisplayColumnDef, + 'cell' | 'header' | 'footer' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > +} + +/** + * Enhanced group column definition with pre-bound components. + */ +type AppGroupColumnDef< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + GroupColumnDef, + 'cell' | 'header' | 'footer' | 'columns' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > + columns?: Array> +} + +// ============================================================================= +// Enhanced Column Helper Type +// ============================================================================= + +/** + * Enhanced column helper with pre-bound components in cell/header/footer contexts. + * This enables TypeScript to know about the registered components when defining columns. + */ +export type AppColumnHelper< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = { + /** + * Creates a data column definition with an accessor key or function. + * The cell, header, and footer contexts include pre-bound components. + */ + accessor: < + TAccessor extends AccessorFn | DeepKeys, + TValue extends TAccessor extends AccessorFn + ? TReturn + : TAccessor extends DeepKeys + ? DeepValue + : never, + >( + accessor: TAccessor, + column: TAccessor extends AccessorFn + ? AppColumnDefBase< + TFeatures, + TData, + TValue, + TCellComponents, + THeaderComponents + > & { id: string } + : AppColumnDefBase< + TFeatures, + TData, + TValue, + TCellComponents, + THeaderComponents + >, + ) => TAccessor extends AccessorFn + ? AccessorFnColumnDef + : AccessorKeyColumnDef + + /** + * Wraps an array of column definitions to preserve each column's individual TValue type. + */ + columns: >>( + columns: [...TColumns], + ) => Array> & [...TColumns] + + /** + * Creates a display column definition for non-data columns. + * The cell, header, and footer contexts include pre-bound components. + */ + display: ( + column: AppDisplayColumnDef< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >, + ) => DisplayColumnDef + + /** + * Creates a group column definition with nested child columns. + * The cell, header, and footer contexts include pre-bound components. + */ + group: ( + column: AppGroupColumnDef< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >, + ) => GroupColumnDef +} + +/** + * Extended table API returned by useAppTable with all App wrapper components + */ +export type AppAngularTable< + TFeatures extends TableFeatures, + TData extends RowData, + TSelected, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +> = AngularTable & NoInfer + +// ============================================================================= +// CreateTableHook Options and Props +// ============================================================================= + +/** + * Options for creating a table hook with pre-bound components and default table options. + * Extends all TableOptions except 'columns' | 'data' | 'store' | 'state' | 'initialState'. + */ +export type CreateTableContextOptions< + TFeatures extends TableFeatures, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + TableOptions, + 'columns' | 'data' | 'store' | 'state' | 'initialState' +> & { + /** + * Table-level components that need access to the table instance. + * These are available directly on the table object returned by useAppTable. + * Use `useTableContext()` inside these components. + * @example { PaginationControls, GlobalFilter, RowCount } + */ + tableComponents?: TTableComponents + /** + * Cell-level components that need access to the cell instance. + * These are available on the cell object passed to AppCell's children. + * Use `useCellContext()` inside these components. + * @example { TextCell, NumberCell, DateCell, CurrencyCell } + */ + cellComponents?: TCellComponents + /** + * Header-level components that need access to the header instance. + * These are available on the header object passed to AppHeader/AppFooter's children. + * Use `useHeaderContext()` inside these components. + * @example { SortIndicator, ColumnFilter, ResizeHandle } + */ + headerComponents?: THeaderComponents +} + +/** + * Props for AppCell component + */ +export interface AppCellPropsWithoutSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, +> { + cell: Cell +} + +export function createTableContexts< + TFeatures extends TableFeatures, + const TTableComponents extends Record, + const TCellComponents extends Record, + const THeaderComponents extends Record, +>({ + tableComponents, + ...defaultTableOptions +}: CreateTableContextOptions< + TFeatures, + TTableComponents, + TCellComponents, + THeaderComponents +>) { + function injectTableContext() { + return _injetTableContext() + } + + function injectTableHeaderContext() { + return _injectTableHeaderContext() + } + + function injectTableCellContext() { + return _injectTableCellContext() + } + + function injectAppTable( + tableOptions: () => Omit< + TableOptions, + '_features' | '_rowModels' + >, + selector?: (state: TableState) => TSelected, + ): AppAngularTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > { + const table = injectTable( + () => + ({ ...defaultTableOptions, ...tableOptions() }) as TableOptions< + TFeatures, + TData + >, + selector, + ) + + function AppCell(props) {} + + const extendedTable = Object.assign(table, { + AppCell, + ...tableComponents, + }) as AngularTable + + return extendedTable + } + + function createAppColumnHelper(): AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > { + // The runtime implementation is the same - components are attached at render time + // This cast provides the enhanced types for column definitions + return coreCreateColumnHelper() as AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > + } + + return { + createAppColumnHelper, + injectTableContext, + injectTableHeaderContext, + injectTableCellContext, + injectAppTable, + } +} diff --git a/packages/angular-table/src/context/flexRender.ts b/packages/angular-table/src/context/flexRender.ts new file mode 100644 index 0000000000..ae87586197 --- /dev/null +++ b/packages/angular-table/src/context/flexRender.ts @@ -0,0 +1 @@ +export function flexRender() {} diff --git a/packages/angular-table/src/context/header.ts b/packages/angular-table/src/context/header.ts new file mode 100644 index 0000000000..f8d7a3d81d --- /dev/null +++ b/packages/angular-table/src/context/header.ts @@ -0,0 +1,33 @@ +import { Directive, inject, input } from '@angular/core' +import { CellData, Header, RowData, TableFeatures } from '@tanstack/table-core' +import type { Signal } from '@angular/core' + +export interface TanStackTableHeaderContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +> { + header: Signal> +} + +@Directive({ + selector: '[tanStackTableHeader]', + exportAs: 'header', +}) +export class TanStackTableHeader< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +> implements TanStackTableHeaderContext { + readonly header = input.required>({ + alias: 'tanStackTableHeader', + }) +} + +export function injectTableHeaderContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +>(): TanStackTableHeaderContext { + return inject(TanStackTableHeader) +} diff --git a/packages/angular-table/src/context/table.ts b/packages/angular-table/src/context/table.ts new file mode 100644 index 0000000000..6e70b692d2 --- /dev/null +++ b/packages/angular-table/src/context/table.ts @@ -0,0 +1,30 @@ +import { Directive, inject, input } from '@angular/core' +import { RowData, Table, TableFeatures } from '@tanstack/table-core' +import type { Signal } from '@angular/core' + +export interface TanStackTableContext< + TFeatures extends TableFeatures, + TData extends RowData, +> { + table: Signal> +} + +@Directive({ + selector: '[tanStackTable]', + exportAs: 'table', +}) +export class TanStackTable< + TFeatures extends TableFeatures, + TData extends RowData, +> implements TanStackTableContext { + readonly table = input.required>({ + alias: 'tanStackTable', + }) +} + +export function injectTableContext< + TFeatures extends TableFeatures, + TData extends RowData, +>(): TanStackTableContext { + return inject(TanStackTable) +} diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index e699a81210..707e644627 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -73,6 +73,7 @@ export class FlexRender< | ((props: TProps) => FlexRenderContent) | null | undefined + | any >({ alias: 'flexRender', }) @@ -81,7 +82,7 @@ export class FlexRender< alias: 'flexRenderProps', }) - readonly notifier = input<'doCheck' | 'tableChange'>('tableChange', { + readonly notifier = input<'doCheck' | 'tableChange'>('doCheck', { alias: 'flexRenderNotifier', }) diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index 30cd0760fc..24e632461c 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -7,3 +7,8 @@ export * from './injectTable' export * from './lazySignalInitializer' export * from './reactivityUtils' export * from './flex-render/flex-render-component' + +export * from './context/cell' +export * from './context/header' +export * from './context/table' +export * from './context/createTableContexts' diff --git a/packages/angular-table/src/lazySignalInitializer.ts b/packages/angular-table/src/lazySignalInitializer.ts index 92f8dcc901..43e3233a7b 100644 --- a/packages/angular-table/src/lazySignalInitializer.ts +++ b/packages/angular-table/src/lazySignalInitializer.ts @@ -6,10 +6,17 @@ import { untracked } from '@angular/core' */ export function lazyInit(initializer: () => T): T { let object: T | null = null + const addedPropsDuringInitialization = {} const initializeObject = () => { if (!object) { - object = untracked(() => initializer()) + object = untracked(() => { + let result = initializer() + if (Object.keys(addedPropsDuringInitialization).length > 0) { + result = Object.assign(result, { ...addedPropsDuringInitialization }) + } + return result + }) } } @@ -29,6 +36,14 @@ export function lazyInit(initializer: () => T): T { initializeObject() return Reflect.get(object as T, prop, receiver) }, + set(target: T, p: string | symbol, newValue: any, receiver: any): boolean { + if (!object) { + addedPropsDuringInitialization[p] = newValue + } + + Reflect.set(target, p, newValue, receiver) + return true + }, has(_, prop) { initializeObject() return Reflect.has(object as T, prop) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0721ca44a1..b9fa59ccc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -390,6 +390,61 @@ importers: specifier: 5.9.3 version: 5.9.3 + examples/angular/composable-tables: + dependencies: + '@angular/common': + specifier: ^21.0.6 + version: 21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.0.6 + version: 21.0.6 + '@angular/core': + specifier: ^21.0.6 + version: 21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/forms': + specifier: ^21.0.6 + version: 21.0.6(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + '@angular/platform-browser': + specifier: ^21.0.6 + version: 21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)) + '@angular/platform-browser-dynamic': + specifier: ^21.0.6 + version: 21.0.6(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@21.0.6)(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))) + '@angular/router': + specifier: ^21.0.6 + version: 21.0.6(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + '@tanstack/angular-table': + specifier: ^9.0.0-alpha.10 + version: link:../../../packages/angular-table + rxjs: + specifier: ~7.8.2 + version: 7.8.2 + zone.js: + specifier: ~0.16.0 + version: 0.16.0 + devDependencies: + '@angular/build': + specifier: ^21.0.4 + version: 21.0.4(kc35yzw5n5t7efydd2g6bmpsfy) + '@angular/cli': + specifier: ^21.0.4 + version: 21.0.4(@types/node@25.0.3)(chokidar@4.0.3) + '@angular/compiler-cli': + specifier: ^21.0.6 + version: 21.0.6(@angular/compiler@21.0.6)(typescript@5.9.3) + '@types/jasmine': + specifier: ~5.1.13 + version: 5.1.13 + jasmine-core: + specifier: ~5.13.0 + version: 5.13.0 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + examples/angular/editable: dependencies: '@angular/animations': From 6e77de336ba03d71695774b90daff98920d59e76 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:20:06 +0000 Subject: [PATCH 02/30] ci: apply automated fixes --- .../src/app/app.component.html | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index 79fb40bfa3..f4619fda1f 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -1,23 +1,24 @@
- - + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - + @for (header of headerGroup.headers; track header.id) { @if (!header.isPlaceholder) { -
- +
@@ -36,10 +37,10 @@
@@ -55,10 +56,10 @@
{{ footer }} @@ -68,7 +69,6 @@ }
-
From 8a51b525e6fc8de9becf3d74821876aa97621f9b Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 8 Jan 2026 21:33:31 +0100 Subject: [PATCH 03/30] update composable table example --- .../src/app/app.component.html | 44 ++-- .../src/app/app.component.ts | 38 +-- .../src/app/components/cell-components.ts | 129 ++++++++++ .../src/app/components/header-components.ts | 48 +++- .../src/app/components/table-components.ts | 10 +- .../composable-tables/src/app/table.ts | 34 ++- .../angular/composable-tables/src/styles.scss | 227 +++++++++++++++++- 7 files changed, 457 insertions(+), 73 deletions(-) create mode 100644 examples/angular/composable-tables/src/app/components/cell-components.ts diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index f4619fda1f..ddd41c409c 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -6,6 +6,7 @@ inputs: { title: 'Users Table', onRefresh } " /> + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { @@ -13,14 +14,8 @@ @for (header of headerGroup.headers; track header.id) { @if (!header.isPlaceholder) { } @@ -31,38 +26,27 @@ @for (row of table.getRowModel().rows; track row.id) { - @for (c of row.getAllCells(); track c.id) { - @let cell = table.AppCell(c); - - } } + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { @for (footer of footerGroup.headers; track footer.id) { - } diff --git a/examples/angular/composable-tables/src/app/app.component.ts b/examples/angular/composable-tables/src/app/app.component.ts index f9d6b8d1bd..3f0b42bdcf 100644 --- a/examples/angular/composable-tables/src/app/app.component.ts +++ b/examples/angular/composable-tables/src/app/app.component.ts @@ -1,8 +1,11 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { + CellFlexRender, FlexRender, TanStackTable, + TanStackTableCell, TanStackTableHeader, + flexRenderComponent, } from '@tanstack/angular-table' import { NgComponentOutlet } from '@angular/common' import { createAppColumnHelper, injectAppTable } from './table' @@ -15,7 +18,14 @@ const productColumnHelper = createAppColumnHelper() @Component({ selector: 'app-root', - imports: [FlexRender, TanStackTableHeader, NgComponentOutlet, TanStackTable], + imports: [ + FlexRender, + TanStackTableHeader, + TanStackTableCell, + NgComponentOutlet, + TanStackTable, + CellFlexRender, + ], templateUrl: './app.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -25,38 +35,38 @@ export class AppComponent { readonly columns = personColumnHelper.columns([ personColumnHelper.accessor('firstName', { header: 'First Name', - footer: (props) => props.column.id, - // cell: ({ cell }) => , + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => flexRenderComponent(cell.TextCell), }), personColumnHelper.accessor('lastName', { header: 'Last Name', - footer: (props) => props.column.id, - // cell: ({ cell }) => , + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => flexRenderComponent(cell.TextCell), }), personColumnHelper.accessor('age', { header: 'Age', - footer: (props) => props.column.id, - // cell: ({ cell }) => , + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => flexRenderComponent(cell.NumberCell), }), personColumnHelper.accessor('visits', { header: 'Visits', - footer: (props) => props.column.id, - // cell: ({ cell }) => , + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => flexRenderComponent(cell.NumberCell), }), personColumnHelper.accessor('status', { header: 'Status', - footer: (props) => props.column.id, - // cell: ({ cell }) => , + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => cell.StatusCell, }), personColumnHelper.accessor('progress', { header: 'Progress', - footer: (props) => props.column.id, - // cell: ({ cell }) => , + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => cell.ProgressCell, }), personColumnHelper.display({ id: 'actions', header: 'Actions', - // cell: ({ cell }) => , + cell: ({ cell }) => cell.RowActionsCell, }), ]) diff --git a/examples/angular/composable-tables/src/app/components/cell-components.ts b/examples/angular/composable-tables/src/app/components/cell-components.ts new file mode 100644 index 0000000000..3ebf7f1426 --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/cell-components.ts @@ -0,0 +1,129 @@ +// /** +// * Cell-level components that use useCellContext +// * +// * These components can be used via the pre-bound cellComponents +// * in AppCell children, e.g., +// */ +// import { useCellContext } from '../hooks/table' +// + +import { Component, computed } from '@angular/core' +import { injectFlexRenderContext } from '@tanstack/angular-table' +import { CurrencyPipe } from '@angular/common' +import { injectTableCellContext } from '../table' +import type { CellContext, TableFeatures } from '@tanstack/angular-table' + +@Component({ + selector: 'span', + host: { + 'tanstack-table-text-cell': '', + }, + template: ` {{ cell.getValue() }} `, +}) +export class TextCell { + readonly cell = + injectFlexRenderContext>() +} + +@Component({ + selector: 'span', + host: { + 'tanstack-table-number-cell': '', + }, + template: ` {{ cell.getValue().toLocaleString() }} `, +}) +export class NumberCell { + readonly cell = + injectFlexRenderContext>() +} + +@Component({ + selector: 'span', + host: { + 'tanstack-table-status-cell': '', + '[class.status-badge]': 'true', + '[class]': 'cell().getValue()', + }, + template: ` {{ cell().getValue() }} `, +}) +export class StatusCell { + readonly cell = injectTableCellContext< + 'relationship' | 'complicated' | 'single' + >() +} + +@Component({ + selector: 'table-progress-cell', + template: ` +
+
+ `, +}) +export class ProgressCell { + readonly cell = injectTableCellContext() + + readonly progress = computed(() => this.cell().getValue()) +} + +@Component({ + selector: 'table-progress-cell', + template: ` +
+ + + +
+ `, +}) +export class RowActionsCell { + readonly cell = injectTableCellContext() + + view() { + alert( + `View: ${this.cell().row.original.firstName} ${this.cell().row.original.lastName}`, + ) + } + + edit() { + alert( + `Edit: ${this.cell().row.original.firstName} ${this.cell().row.original.lastName}`, + ) + } + + delete() { + alert( + `Delete: ${this.cell().row.original.firstName} ${this.cell().row.original.lastName}`, + ) + } +} + +@Component({ + selector: 'table-price-cell', + template: ` {{ price() | currency }} `, + imports: [CurrencyPipe], +}) +export class PriceCell { + readonly cell = injectTableCellContext() + + readonly price = computed(() => + this.cell().getValue().toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + ) +} + +@Component({ + selector: 'span', + host: { + 'tanstack-table-category-cell': '', + '[class.category-badge]': 'true', + '[class]': 'cell().getValue()', + }, + template: ` {{ cell().getValue() }} `, +}) +export class CategoryCell { + readonly cell = injectTableCellContext< + 'electronics' | 'clothing' | 'food' | 'books' + >() +} diff --git a/examples/angular/composable-tables/src/app/components/header-components.ts b/examples/angular/composable-tables/src/app/components/header-components.ts index f52f3210dc..168c204edf 100644 --- a/examples/angular/composable-tables/src/app/components/header-components.ts +++ b/examples/angular/composable-tables/src/app/components/header-components.ts @@ -9,16 +9,50 @@ // ) // } -import { Component } from '@angular/core' -import { injectTableHeaderContext } from '@tanstack/angular-table' +import { Component, computed } from '@angular/core' +import { injectTableHeaderContext } from '../table' + +// @Component({ +// selector: 'app-sort-indicator', +// host: { +// class: 'sort-indicator', +// }, +// template: ` {{ sorted === 'asc' ? '🔼' : '🔽' }} `, +// }) +// export class SortIndicator { +// readonly context = injectTableHeaderContext() +// } + +@Component({ + selector: 'span', + host: { + 'tantack-footer-column-id': '', + class: 'footer-column-id', + }, + template: `{{ header().column.id }}`, +}) +export class FooterColumnId { + readonly header = injectTableHeaderContext() +} @Component({ - selector: 'app-sort-indicator', + selector: 'span', host: { - class: 'sort-indicator', + 'tantack-footer-sum': '', + class: 'footer-sum', }, - template: ` {{ sorted === 'asc' ? '🔼' : '🔽' }} `, + template: `{{ sum() > 0 ? sum().toLocaleString() : '—' }}`, }) -export class SortIndicator { - readonly context = injectTableHeaderContext() +export class FooterSum { + readonly header = injectTableHeaderContext() + + readonly table = computed(() => this.header().getContext().table) + readonly rows = computed(() => this.table().getFilteredRowModel().rows) + + readonly sum = computed(() => + this.rows().reduce((acc, row) => { + const value = row.getValue(this.header().column.id) + return acc + (typeof value === 'number' ? value : 0) + }, 0), + ) } diff --git a/examples/angular/composable-tables/src/app/components/table-components.ts b/examples/angular/composable-tables/src/app/components/table-components.ts index 6d752982c2..fd9734be2f 100644 --- a/examples/angular/composable-tables/src/app/components/table-components.ts +++ b/examples/angular/composable-tables/src/app/components/table-components.ts @@ -5,10 +5,8 @@ import { injectTableContext } from '../table' template: `

{{ title() }}

- - + + @if (onRefresh(); as onRefresh) { @@ -21,9 +19,9 @@ export class TableToolbar { readonly title = input.required() readonly onRefresh = input<() => void>() - readonly context = injectTableContext() + readonly table = injectTableContext() constructor() { - this.context.table().resetColumnFilters() + this.table().resetColumnFilters() } } diff --git a/examples/angular/composable-tables/src/app/table.ts b/examples/angular/composable-tables/src/app/table.ts index 33f411d8cb..a4a6ef2bb4 100644 --- a/examples/angular/composable-tables/src/app/table.ts +++ b/examples/angular/composable-tables/src/app/table.ts @@ -10,7 +10,6 @@ import { createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, - createTableContexts, filterFns, rowPaginationFeature, rowSortingFeature, @@ -18,7 +17,18 @@ import { tableFeatures, } from '@tanstack/angular-table' // Import table-level components +import { createTableHook } from '@tanstack/angular-table' import { TableToolbar } from './components/table-components' +import { + CategoryCell, + NumberCell, + PriceCell, + ProgressCell, + RowActionsCell, + StatusCell, + TextCell, +} from './components/cell-components' +import { FooterColumnId, FooterSum } from './components/header-components' // Import table-level components // import { @@ -44,11 +54,13 @@ export const { createAppColumnHelper, injectAppTable, injectTableContext, + injectTableCellContext, + injectTableHeaderContext, // useAppTable, // useTableContext, // useCellContext, // useHeaderContext, -} = createTableContexts({ +} = createTableHook({ // Features are set once here and shared across all tables _features: tableFeatures({ columnFilteringFeature, @@ -75,20 +87,20 @@ export const { // Register cell-level components (accessible via cell.ComponentName in AppCell) cellComponents: { - // TextCell, - // NumberCell, - // StatusCell, - // ProgressCell, - // RowActionsCell, - // PriceCell, - // CategoryCell, + TextCell, + NumberCell, + ProgressCell, + StatusCell, + CategoryCell, + PriceCell, + RowActionsCell, }, // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) headerComponents: { // SortIndicator, // ColumnFilter, - // FooterColumnId, - // FooterSum, + FooterColumnId, + FooterSum, }, }) diff --git a/examples/angular/composable-tables/src/styles.scss b/examples/angular/composable-tables/src/styles.scss index cda3113f7d..d7a5ed6c49 100644 --- a/examples/angular/composable-tables/src/styles.scss +++ b/examples/angular/composable-tables/src/styles.scss @@ -5,16 +5,25 @@ html { table { border: 1px solid lightgray; + border-collapse: collapse; + width: 100%; } tbody { border-bottom: 1px solid lightgray; } -th { +th, +td { border-bottom: 1px solid lightgray; border-right: 1px solid lightgray; - padding: 2px 4px; + padding: 8px 12px; + text-align: left; +} + +th { + background-color: #f5f5f5; + font-weight: 600; } tfoot { @@ -25,8 +34,216 @@ tfoot th { font-weight: normal; } -.pagination-actions { - margin: 10px; +.table-container { + padding: 16px; + max-width: 1200px; + margin: 0 auto; +} + +.pagination { display: flex; - gap: 10px; + align-items: center; + gap: 8px; + margin-top: 16px; + flex-wrap: wrap; +} + +.pagination button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + background: white; +} + +.pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pagination input { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; + width: 64px; +} + +.pagination select { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; +} + +.sort-indicator { + margin-left: 4px; +} + +.column-filter { + margin-top: 4px; +} + +.column-filter input { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; + width: 100%; + font-size: 12px; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.sortable-header:hover { + background-color: #e8e8e8; +} + +.row-actions { + display: flex; + gap: 4px; +} + +.row-actions button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px 8px; + cursor: pointer; + background: white; + font-size: 12px; +} + +.row-actions button:hover { + background-color: #f0f0f0; +} + +.status-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.relationship { + background-color: #d4edda; + color: #155724; +} + +.status-badge.complicated { + background-color: #fff3cd; + color: #856404; +} + +.status-badge.single { + background-color: #cce5ff; + color: #004085; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #e9ecef; + border-radius: 4px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background-color: #007bff; + transition: width 0.2s; +} + +.table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 8px; +} + +.table-toolbar h2 { + margin: 0; +} + +.table-toolbar button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px 16px; + cursor: pointer; + background: white; +} + +.table-toolbar button:hover { + background-color: #f0f0f0; +} + +.row-count { + color: #666; + font-size: 14px; + margin-top: 8px; +} + +.app { + padding: 16px; +} + +.app h1 { + text-align: center; + margin-bottom: 8px; +} + +.description { + text-align: center; + color: #666; + margin-bottom: 32px; +} + +.description code { + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; +} + +.table-divider { + height: 48px; + border-bottom: 1px solid #e0e0e0; + margin: 32px auto; + max-width: 1200px; +} + +.category-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: capitalize; +} + +.category-badge.electronics { + background-color: #e3f2fd; + color: #1565c0; +} + +.category-badge.clothing { + background-color: #fce4ec; + color: #c2185b; +} + +.category-badge.food { + background-color: #e8f5e9; + color: #2e7d32; +} + +.category-badge.books { + background-color: #fff8e1; + color: #f57c00; +} + +.price { + font-weight: 600; + color: #2e7d32; } From 2df745b56b86eb542b0cb5d0973010d2c5c30888 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 8 Jan 2026 21:34:43 +0100 Subject: [PATCH 04/30] add some directives to provide table context --- packages/angular-table/src/context/cell.ts | 61 +++++++++++++++++-- ...ateTableContexts.ts => createTableHook.ts} | 40 ++++++------ .../angular-table/src/context/flexRender.ts | 1 - packages/angular-table/src/context/header.ts | 4 +- packages/angular-table/src/context/table.ts | 4 +- packages/angular-table/src/flex-render.ts | 10 +-- packages/angular-table/src/index.ts | 2 +- packages/angular-table/src/reactivityUtils.ts | 51 +++++++++------- 8 files changed, 118 insertions(+), 55 deletions(-) rename packages/angular-table/src/context/{createTableContexts.ts => createTableHook.ts} (93%) delete mode 100644 packages/angular-table/src/context/flexRender.ts diff --git a/packages/angular-table/src/context/cell.ts b/packages/angular-table/src/context/cell.ts index 356ad17eaf..864b39d0d6 100644 --- a/packages/angular-table/src/context/cell.ts +++ b/packages/angular-table/src/context/cell.ts @@ -1,6 +1,9 @@ -import { Directive, inject, input } from '@angular/core' +import { Directive, effect, inject, input } from '@angular/core' import { Cell, CellData, RowData, TableFeatures } from '@tanstack/table-core' +import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' +import { FlexRender } from '../flex-render' import type { Signal } from '@angular/core' +import type { Header } from '@tanstack/table-core' export interface TanStackTableCellContext< TFeatures extends TableFeatures, @@ -19,13 +22,63 @@ export class TanStackTableCell< TData extends RowData, TValue extends CellData, > implements TanStackTableCellContext { - readonly cell = input.required>() + readonly cell = input.required>({ + alias: 'tanStackTableCell', + }) } export function injectTableCellContext< TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData, ->(): TanStackTableCellContext { - return inject(TanStackTableCell) +>(): TanStackTableCellContext['cell'] { + return inject(TanStackTableCell).cell +} + +@Directive({ + selector: + 'ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]', + hostDirectives: [{ directive: FlexRender }], +}) +export class CellFlexRender< + TFeatures extends TableFeatures, + TData extends RowData, + TValue, +> { + readonly flexRender = inject(FlexRender) + + readonly cell = input>(undefined, { + alias: 'flexRenderCell', + }) + + readonly header = input>(undefined, { + alias: 'flexRenderHeader', + }) + + readonly footer = input>(undefined, { + alias: 'flexRenderFooter', + }) + + constructor() { + effect(() => { + const cell = this.cell() + const header = this.header() + const footer = this.footer() + const contentNode = this.flexRender.content[SIGNAL] + const propsNode = this.flexRender.props[SIGNAL] + + if (cell) { + signalSetFn(contentNode, cell.column.columnDef.cell) + signalSetFn(propsNode, cell.getContext()) + } + if (header) { + signalSetFn(contentNode, header.column.columnDef.header) + signalSetFn(propsNode, header.getContext()) + } + if (footer) { + signalSetFn(contentNode, footer.column.columnDef.footer) + signalSetFn(propsNode, footer.getContext()) + } + }) + } } diff --git a/packages/angular-table/src/context/createTableContexts.ts b/packages/angular-table/src/context/createTableHook.ts similarity index 93% rename from packages/angular-table/src/context/createTableContexts.ts rename to packages/angular-table/src/context/createTableHook.ts index 0bd001a0e7..f7c888cd4c 100644 --- a/packages/angular-table/src/context/createTableContexts.ts +++ b/packages/angular-table/src/context/createTableHook.ts @@ -22,6 +22,7 @@ import type { Row, RowData, Table, + TableFeature, TableFeatures, TableOptions, TableState, @@ -298,13 +299,15 @@ export interface AppCellPropsWithoutSelector< cell: Cell } -export function createTableContexts< +export function createTableHook< TFeatures extends TableFeatures, const TTableComponents extends Record, const TCellComponents extends Record, const THeaderComponents extends Record, >({ tableComponents, + cellComponents, + headerComponents, ...defaultTableOptions }: CreateTableContextOptions< TFeatures, @@ -338,23 +341,26 @@ export function createTableContexts< TCellComponents, THeaderComponents > { - const table = injectTable( - () => - ({ ...defaultTableOptions, ...tableOptions() }) as TableOptions< - TFeatures, - TData - >, - selector, - ) - - function AppCell(props) {} - - const extendedTable = Object.assign(table, { - AppCell, - ...tableComponents, - }) as AngularTable + const appTableFeatures: TableFeature<{}> = { + constructTableAPIs: (table) => { + Object.assign(table, tableComponents) + }, + assignCellPrototype(prototype) { + Object.assign(prototype, cellComponents) + }, + assignHeaderPrototype(prototype) { + Object.assign(prototype, headerComponents) + }, + } - return extendedTable + return injectTable(() => { + const options = { + ...defaultTableOptions, + ...tableOptions(), + } as TableOptions + options._features = { ...options._features, appTableFeatures } + return options + }, selector) as AngularTable } function createAppColumnHelper(): AppColumnHelper< diff --git a/packages/angular-table/src/context/flexRender.ts b/packages/angular-table/src/context/flexRender.ts deleted file mode 100644 index ae87586197..0000000000 --- a/packages/angular-table/src/context/flexRender.ts +++ /dev/null @@ -1 +0,0 @@ -export function flexRender() {} diff --git a/packages/angular-table/src/context/header.ts b/packages/angular-table/src/context/header.ts index f8d7a3d81d..61d2adf9b6 100644 --- a/packages/angular-table/src/context/header.ts +++ b/packages/angular-table/src/context/header.ts @@ -28,6 +28,6 @@ export function injectTableHeaderContext< TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData, ->(): TanStackTableHeaderContext { - return inject(TanStackTableHeader) +>(): TanStackTableHeaderContext['header'] { + return inject(TanStackTableHeader).header } diff --git a/packages/angular-table/src/context/table.ts b/packages/angular-table/src/context/table.ts index 6e70b692d2..9f6cdb9421 100644 --- a/packages/angular-table/src/context/table.ts +++ b/packages/angular-table/src/context/table.ts @@ -25,6 +25,6 @@ export class TanStackTable< export function injectTableContext< TFeatures extends TableFeatures, TData extends RowData, ->(): TanStackTableContext { - return inject(TanStackTable) +>(): TanStackTableContext['table'] { + return inject(TanStackTable).table } diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index 707e644627..c95be3b093 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -67,18 +67,18 @@ export class FlexRender< readonly #flexRenderComponentFactory = inject(FlexRenderComponentFactory) readonly #changeDetectorRef = inject(ChangeDetectorRef) - readonly content = input.required< + readonly content = input< | number | string | ((props: TProps) => FlexRenderContent) | null | undefined | any - >({ + >(undefined, { alias: 'flexRender', }) - readonly props = input.required({ + readonly props = input({} as TProps, { alias: 'flexRenderProps', }) @@ -114,13 +114,13 @@ export class FlexRender< }) ngOnChanges(changes: SimpleChanges>) { - if (changes['props']) { + if (changes.props) { const props = changes.props.currentValue this.table = 'table' in props ? props.table : null this.renderFlags |= FlexRenderFlags.PropsReferenceChanged this.bindTableDirtyCheck() } - if (changes['content']) { + if (changes.content) { this.renderFlags |= FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender this.update() diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index 24e632461c..52cd4133e1 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -11,4 +11,4 @@ export * from './flex-render/flex-render-component' export * from './context/cell' export * from './context/header' export * from './context/table' -export * from './context/createTableContexts' +export * from './context/createTableHook' diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts index eb02ce9906..009eb17ad3 100644 --- a/packages/angular-table/src/reactivityUtils.ts +++ b/packages/angular-table/src/reactivityUtils.ts @@ -142,28 +142,33 @@ export function assignReactivePrototypeAPI( fnName: string, ) { const fn = prototype[fnName] - const originalArgsLength = Math.max( - 0, - Reflect.get(fn, 'originalArgsLength') ?? 0, - ) - - if (originalArgsLength <= 1) { - const cached = {} as Record> - Object.defineProperty(prototype, fnName, { - enumerable: true, - configurable: true, - get(this) { - const self = this - return (cached[`${self.id}_${fnName}`] ??= computed(() => { - notifier() - return fn.call(self) - })) - }, - }) - } else { - prototype[fnName] = function (this: unknown, ...args: Array) { - notifier() - return fn.apply(this, args) - } + // const originalArgsLength = Math.max( + // 0, + // Reflect.get(fn, 'originalArgsLength') ?? 0, + // ) + + // if (originalArgsLength <= 1) { + prototype[fnName] = function (this: unknown, ...args: Array) { + notifier() + return fn.apply(this, args) } + // TODO: this break everything. for example, when table data changes, it still uses old cell + // const cached = {} as Record> + // Object.defineProperty(prototype, fnName, { + // enumerable: true, + // configurable: true, + // get(this) { + // const self = this + // return (cached[`${self.id}_${fnName}`] ??= computed(() => { + // notifier() + // return fn.call(self) + // }, {})) + // }, + // }) + // } else { + // prototype[fnName] = function (this: unknown, ...args: Array) { + // notifier() + // return fn.apply(this, args) + // } + // } } From c085061c5062781baefd5192a45e44e2b7ec3171 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 8 Jan 2026 21:34:54 +0100 Subject: [PATCH 05/30] remove lazyInit set property workaround --- .../angular-table/src/lazySignalInitializer.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/angular-table/src/lazySignalInitializer.ts b/packages/angular-table/src/lazySignalInitializer.ts index 43e3233a7b..1d092d68d7 100644 --- a/packages/angular-table/src/lazySignalInitializer.ts +++ b/packages/angular-table/src/lazySignalInitializer.ts @@ -10,13 +10,7 @@ export function lazyInit(initializer: () => T): T { const initializeObject = () => { if (!object) { - object = untracked(() => { - let result = initializer() - if (Object.keys(addedPropsDuringInitialization).length > 0) { - result = Object.assign(result, { ...addedPropsDuringInitialization }) - } - return result - }) + object = untracked(() => initializer()) } } @@ -36,14 +30,6 @@ export function lazyInit(initializer: () => T): T { initializeObject() return Reflect.get(object as T, prop, receiver) }, - set(target: T, p: string | symbol, newValue: any, receiver: any): boolean { - if (!object) { - addedPropsDuringInitialization[p] = newValue - } - - Reflect.set(target, p, newValue, receiver) - return true - }, has(_, prop) { initializeObject() return Reflect.has(object as T, prop) From 1b4508b0772ffe748787febd09024502e61dd241 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:36:28 +0000 Subject: [PATCH 06/30] ci: apply automated fixes --- examples/angular/composable-tables/src/app/app.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index ddd41c409c..50ae2b1806 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -29,7 +29,7 @@ @for (cell of row.getAllCells(); track cell.id) {
} From 2dd1df385b4f35965b1bd7545bc9b5456b0a3c3a Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Fri, 9 Jan 2026 22:46:41 +0100 Subject: [PATCH 07/30] fix garbage collection issues in angular --- packages/angular-table/src/flex-render.ts | 12 +++- .../angular-table/src/flex-render/view.ts | 10 ++++ packages/angular-table/src/reactivityUtils.ts | 58 +++++++++---------- packages/table-core/src/utils.ts | 8 ++- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index c95be3b093..143dcbcebd 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -2,6 +2,7 @@ import { ChangeDetectorRef, Directive, DoCheck, + EffectRef, Injector, OnChanges, SimpleChanges, @@ -34,7 +35,6 @@ import type { Table, TableFeatures, } from '@tanstack/table-core' -import type { EffectRef } from '@angular/core' export { injectFlexRenderContext, @@ -86,7 +86,8 @@ export class FlexRender< alias: 'flexRenderNotifier', }) - readonly injector = input(inject(Injector), { + readonly #injector = inject(Injector) + readonly injector = input(this.#injector, { alias: 'flexRenderInjector', }) @@ -172,7 +173,7 @@ export class FlexRender< this.renderFlags |= FlexRenderFlags.DirtyCheck this.doCheck() }, - { injector: this.injector() }, + { injector: this.#injector }, ) } } @@ -210,6 +211,11 @@ export class FlexRender< } this.viewContainerRef.clear() + if (this.renderView) { + this.renderView.unmount() + this.renderView = null + } + this.renderFlags = FlexRenderFlags.Pristine | (this.renderFlags & FlexRenderFlags.ViewFirstRender) | diff --git a/packages/angular-table/src/flex-render/view.ts b/packages/angular-table/src/flex-render/view.ts index fbdda58851..dc08fdce70 100644 --- a/packages/angular-table/src/flex-render/view.ts +++ b/packages/angular-table/src/flex-render/view.ts @@ -67,6 +67,8 @@ export abstract class FlexRenderView< abstract dirtyCheck(): void abstract onDestroy(callback: Function): void + + abstract unmount(): void } export class FlexRenderTemplateView extends FlexRenderView< @@ -95,6 +97,10 @@ export class FlexRenderTemplateView extends FlexRenderView< // this.view.markForCheck() } + override unmount() { + this.view.destroy() + } + override onDestroy(callback: Function) { this.view.onDestroy(callback) } @@ -148,6 +154,10 @@ export class FlexRenderComponentView extends FlexRenderView< } } + override unmount() { + this.view.componentRef.destroy() + } + override onDestroy(callback: Function) { this.view.componentRef.onDestroy(callback) } diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts index 009eb17ad3..6f4dabab28 100644 --- a/packages/angular-table/src/reactivityUtils.ts +++ b/packages/angular-table/src/reactivityUtils.ts @@ -91,7 +91,7 @@ export function toComputed< fn: TFunction, debugName: string, ): ComputedFunction { - const hasArgs = fn.length > 0 + const hasArgs = getFnArgsLength(fn) > 0 if (!hasArgs) { const computedFn = computed( () => { @@ -136,39 +136,39 @@ function serializeArgs(...args: Array) { return JSON.stringify(args) } +function getFnArgsLength( + fn: ((...args: any) => any) & { originalArgsLength?: number }, +): number { + return Math.max(0, fn.originalArgsLength ?? fn.length) +} + export function assignReactivePrototypeAPI( notifier: Signal, prototype: Record, fnName: string, ) { const fn = prototype[fnName] - // const originalArgsLength = Math.max( - // 0, - // Reflect.get(fn, 'originalArgsLength') ?? 0, - // ) - - // if (originalArgsLength <= 1) { - prototype[fnName] = function (this: unknown, ...args: Array) { - notifier() - return fn.apply(this, args) + const originalArgsLength = getFnArgsLength(fn) + + if (originalArgsLength <= 1) { + Object.defineProperty(prototype, fnName, { + enumerable: true, + configurable: true, + get(this) { + const self = this + // Create a cache in the current prototype to allow the signals + // to be garbage collected. Shorthand for a WeakMap implementation + self._reactiveCache ??= {} + return (self._reactiveCache[`${self.id}${fnName}`] ??= computed(() => { + notifier() + return fn.apply(self) + }, {})) + }, + }) + } else { + prototype[fnName] = function (this: unknown, ...args: Array) { + notifier() + return fn.apply(this, args) + } } - // TODO: this break everything. for example, when table data changes, it still uses old cell - // const cached = {} as Record> - // Object.defineProperty(prototype, fnName, { - // enumerable: true, - // configurable: true, - // get(this) { - // const self = this - // return (cached[`${self.id}_${fnName}`] ??= computed(() => { - // notifier() - // return fn.call(self) - // }, {})) - // }, - // }) - // } else { - // prototype[fnName] = function (this: unknown, ...args: Array) { - // notifier() - // return fn.apply(this, args) - // } - // } } diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts index 5734ec3e8a..3dca80a8d1 100755 --- a/packages/table-core/src/utils.ts +++ b/packages/table-core/src/utils.ts @@ -82,7 +82,7 @@ export const memo = , TDepArgs, TResult>({ let deps: Array | undefined = [] let result: TResult | undefined - return (depArgs): TResult => { + const memoizedFn = (depArgs?: TDepArgs): TResult => { onBeforeCompare?.() const newDeps = memoDeps?.(depArgs) const depsChanged = @@ -103,6 +103,12 @@ export const memo = , TDepArgs, TResult>({ return result } + + Object.defineProperties(memoizedFn, { + originalArgsLength: { value: fn.length }, + }) + + return memoizedFn } interface TableMemoOptions< From fe87811bffffd702797cd2f94ec9a86844e8c6fb Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Fri, 9 Jan 2026 23:22:31 +0100 Subject: [PATCH 08/30] fix: avoid creating computed cache for methods that need an object as an argument --- packages/angular-table/src/reactivityUtils.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts index 6f4dabab28..2b45358d53 100644 --- a/packages/angular-table/src/reactivityUtils.ts +++ b/packages/angular-table/src/reactivityUtils.ts @@ -107,11 +107,22 @@ export function toComputed< const computedCache: Record> = {} - const computedFn = (arg0: any, ...otherArgs: Array) => { - const argsArray = [arg0, ...otherArgs] + const computedFn = function (this: unknown, ...argsArray: Array) { + const cacheable = argsArray.every((arg) => { + return ( + arg === null || + arg === undefined || + typeof arg === 'string' || + typeof arg === 'number' || + typeof arg === 'boolean' || + typeof arg === 'symbol' + ) + }) + if (!cacheable) return false + const serializedArgs = serializeArgs(...argsArray) - if (computedCache.hasOwnProperty(serializedArgs)) { - return computedCache[serializedArgs]?.() + if (computedCache[serializedArgs]) { + return computedCache[serializedArgs]() } const computedSignal = computed( () => { From 7bd52bc9f853fb499ca6fe5a499202f6ceaee124 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 10 Jan 2026 14:57:58 +0100 Subject: [PATCH 09/30] refactor toComputed, add some tests --- packages/angular-table/src/reactivityUtils.ts | 59 ++++++++++--------- .../angular-table/tests/injectTable.test.ts | 19 ++++++ .../tests/reactivityUtils.test.ts | 33 ++++++++--- packages/angular-table/tests/test-utils.ts | 15 +++++ 4 files changed, 90 insertions(+), 36 deletions(-) diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts index 2b45358d53..b9b1870b87 100644 --- a/packages/angular-table/src/reactivityUtils.ts +++ b/packages/angular-table/src/reactivityUtils.ts @@ -39,12 +39,12 @@ export function defineLazyComputedProperty( Object.defineProperty(originalObject, property, { enumerable: true, configurable: true, - get(this) { + get() { const computedValue = toComputed(notifier, valueFn, property) markReactive(computedValue) // Once the property is set the first time, we don't need a getter anymore // since we have a computed / cached fn value - Object.defineProperty(this, property, { + Object.defineProperty(originalObject, property, { value: computedValue, configurable: true, enumerable: true, @@ -58,14 +58,12 @@ export function defineLazyComputedProperty( /** * @internal should be used only internally */ -type ComputedFunction = - // 0 args - T extends (...args: []) => infer TReturn - ? Signal - : // 1+ args - T extends (arg0?: any, ...args: Array) => any - ? T - : never +type ComputedFunction = T extends () => infer TReturn + ? Signal + : // 1+ args + T extends (arg0?: any, ...args: Array) => any + ? T + : never /** * @description Transform a function into a computed that react to given notifier re-computations @@ -105,34 +103,37 @@ export function toComputed< return computedFn as ComputedFunction } - const computedCache: Record> = {} - - const computedFn = function (this: unknown, ...argsArray: Array) { - const cacheable = argsArray.every((arg) => { - return ( - arg === null || - arg === undefined || - typeof arg === 'string' || - typeof arg === 'number' || - typeof arg === 'boolean' || - typeof arg === 'symbol' - ) - }) - if (!cacheable) return false - + const computedFn: ((this: unknown, ...argsArray: Array) => unknown) & { + _reactiveCache?: Record> + } = function (this: unknown, ...argsArray: Array) { + const cacheable = + argsArray.length === 0 || + argsArray.every((arg) => { + return ( + arg === null || + arg === undefined || + typeof arg === 'string' || + typeof arg === 'number' || + typeof arg === 'boolean' || + typeof arg === 'symbol' + ) + }) + if (!cacheable) { + return fn.apply(this, argsArray) + } const serializedArgs = serializeArgs(...argsArray) - if (computedCache[serializedArgs]) { - return computedCache[serializedArgs]() + if ((computedFn._reactiveCache ??= {})[serializedArgs]) { + return computedFn._reactiveCache[serializedArgs]() } const computedSignal = computed( () => { void notifier() - return fn(...argsArray) + return fn.apply(this, argsArray) }, { debugName }, ) - computedCache[serializedArgs] = computedSignal + computedFn._reactiveCache[serializedArgs] = computedSignal return computedSignal() } diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index abc4995b34..543fd87edb 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -9,6 +9,7 @@ import { } from '@tanstack/table-core' import { RowModel, injectTable } from '../src' import { + getFnReactiveCache, setFixtureSignalInputs, testShouldBeComputedProperty, } from './test-utils' @@ -178,6 +179,24 @@ describe('injectTable - Experimental reactivity', () => { const tableProperty = table[name as keyof typeof table] expect(isSignal(tableProperty)).toEqual(expected) }) + + describe('will create a computed for non detectable computed properties', () => { + test('getIsSomeRowsPinned', () => { + table.getIsSomeRowsPinned('top') + table.getIsSomeRowsPinned('bottom') + table.getIsSomeRowsPinned() + + expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( + '["top"]', + ) + expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( + '["bottom"]', + ) + expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( + '[]', + ) + }) + }) }) describe('Header property reactivity', () => { diff --git a/packages/angular-table/tests/reactivityUtils.test.ts b/packages/angular-table/tests/reactivityUtils.test.ts index 72c3a5110b..5aff11bf23 100644 --- a/packages/angular-table/tests/reactivityUtils.test.ts +++ b/packages/angular-table/tests/reactivityUtils.test.ts @@ -26,15 +26,15 @@ describe('toComputed', () => { mockFn(result()) }) - TestBed.flushEffects() + TestBed.tick() expect(mockFn).toHaveBeenLastCalledWith(2) notifier.set(3) - TestBed.flushEffects() + TestBed.tick() expect(mockFn).toHaveBeenLastCalledWith(6) notifier.set(2) - TestBed.flushEffects() + TestBed.tick() expect(mockFn).toHaveBeenLastCalledWith(4) expect(mockFn.mock.calls.length).toEqual(3) @@ -53,7 +53,7 @@ describe('toComputed', () => { }, '3args', ) - expect(fn1.length).toEqual(1) + expect(fn1.length).toEqual(0) // currently full rest parameters is not supported const fn2 = toComputed( @@ -143,10 +143,12 @@ describe('toComputed', () => { describe('args 0~1', () => { test('creates a fn an explicit first argument and allows other args', () => { const notifier = signal(1) + const captor = vi.fn<(arg0?: number) => void>() + const captor2 = vi.fn<(arg0?: number) => void>() const fn1 = toComputed( notifier, - (arg0?: number) => { + (arg0: number | undefined) => { if (arg0 === undefined) { return 5 * notifier() } @@ -154,9 +156,26 @@ describe('toComputed', () => { }, 'optionalArgs', ) - expect(fn1.length).toEqual(1) - fn1() + expect(isSignal(fn1)).toEqual(false) + + TestBed.runInInjectionContext(() => { + effect(() => { + captor(fn1(0)) + }) + effect(() => { + captor2(fn1(1)) + }) + }) + + TestBed.tick() + notifier.set(2) + TestBed.tick() + notifier.set(3) + expect(captor.mock.calls).toHaveLength(1) + expect(captor2.mock.calls).toHaveLength(2) + expect(captor2).toHaveBeenNthCalledWith(1, 1) + expect(captor2).toHaveBeenNthCalledWith(2, 2) }) }) }) diff --git a/packages/angular-table/tests/test-utils.ts b/packages/angular-table/tests/test-utils.ts index 920220e3f2..64f1eb2253 100644 --- a/packages/angular-table/tests/test-utils.ts +++ b/packages/angular-table/tests/test-utils.ts @@ -53,10 +53,21 @@ export async function flushQueue() { } const staticComputedProperties = ['get', 'state'] +const staticNonComputedProperties = [ + 'getIsSomeRowsPinned', + 'getColumn', + 'getRowId', + 'getRow', + 'getIsSomeColumnsPinned', +] export const testShouldBeComputedProperty = ( testObj: any, propertyName: string, ) => { + if (staticNonComputedProperties.some((prop) => propertyName === prop)) { + return false + } + if (staticComputedProperties.some((prop) => propertyName === prop)) { return true } @@ -71,3 +82,7 @@ export const testShouldBeComputedProperty = ( } return false } + +export function getFnReactiveCache(fn: any): void { + return fn._reactiveCache +} From 48c70bde2b0c416509ed9dfc9bdda96432fb3892 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 10 Jan 2026 15:36:18 +0100 Subject: [PATCH 10/30] refactor(injectTable): improve type definitions and enhance reactivity in table subscription --- packages/angular-table/src/injectTable.ts | 36 ++- .../tests/angularReactivityFeature.test.ts | 229 ++++++++++++++++++ .../angular-table/tests/injectTable.test.ts | 205 ++-------------- packages/angular-table/tests/test-utils.ts | 2 +- 4 files changed, 268 insertions(+), 204 deletions(-) create mode 100644 packages/angular-table/tests/angularReactivityFeature.test.ts diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index 9c3bf7e99f..e1897e70f4 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -10,7 +10,6 @@ import { constructTable } from '@tanstack/table-core' import { injectStore } from '@tanstack/angular-store' import { lazyInit } from './lazySignalInitializer' import { angularReactivityFeature } from './angularReactivityFeature' -import type { Signal } from '@angular/core' import type { RowData, Table, @@ -18,6 +17,7 @@ import type { TableOptions, TableState, } from '@tanstack/table-core' +import type { Signal, ValueEqualityFn } from '@angular/core' export type AngularTable< TFeatures extends TableFeatures, @@ -33,8 +33,8 @@ export type AngularTable< */ Subscribe: (props: { selector: (state: TableState) => TSubSelected - children: ((state: Signal>) => any) | any - }) => any + equal?: ValueEqualityFn + }) => Signal> } export function injectTable< @@ -58,11 +58,9 @@ export function injectTable< }, } as TableOptions - const table = constructTable(resolvedOptions) as AngularTable< - TFeatures, - TData, - TSelected - > + const table: AngularTable = constructTable( + resolvedOptions, + ) as AngularTable const updatedOptions = computed>(() => { const tableOptionsValue = options() @@ -96,12 +94,9 @@ export function injectTable< const tableSignalNotifier = computed( () => { - // TODO: replace computed just using effects could be better? tableState() table.setOptions(updatedOptions()) - untracked(() => { - table.baseStore.setState((prev) => ({ ...prev })) - }) + untracked(() => table.baseStore.setState((prev) => ({ ...prev }))) return table }, { equal: () => false }, @@ -111,18 +106,17 @@ export function injectTable< table.Subscribe = function Subscribe(props: { selector: (state: TableState) => TSubSelected - children: ((state: Signal>) => any) | any + equal?: ValueEqualityFn }) { - const selected = injectStore(table.store, props.selector, { injector }) - if (typeof props.children === 'function') { - return props.children(selected) - } - return props.children + return injectStore(table.store, props.selector, { + injector, + equal: props.equal, + }) } - const stateStore = injectStore(table.store, selector, { injector }) - - Reflect.set(table, 'state', stateStore) + Object.defineProperty(table, 'state', { + value: injectStore(table.store, selector, { injector }), + }) return table }) diff --git a/packages/angular-table/tests/angularReactivityFeature.test.ts b/packages/angular-table/tests/angularReactivityFeature.test.ts new file mode 100644 index 0000000000..bf7dacb891 --- /dev/null +++ b/packages/angular-table/tests/angularReactivityFeature.test.ts @@ -0,0 +1,229 @@ +import { describe, expect, test, vi } from 'vitest' +import { computed, effect, isSignal, signal } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { injectTable, stockFeatures } from '../src' +import { getFnReactiveCache, testShouldBeComputedProperty } from './test-utils' +import type { WritableSignal } from '@angular/core' +import type { ColumnDef } from '../src' + +describe('angularReactivityFeature', () => { + type Data = { id: string; title: string } + const data = signal>([{ id: '1', title: 'Title' }]) + const columns: Array> = [ + { + id: 'id', + header: 'Id', + accessorKey: 'id', + cell: (context) => context.getValue(), + }, + { + id: 'title', + header: 'Title', + accessorKey: 'title', + cell: (context) => context.getValue(), + }, + ] + + function createTestTable(_data: WritableSignal> = data) { + return TestBed.runInInjectionContext(() => + injectTable(() => ({ + data: _data(), + _features: { ...stockFeatures }, + columns: columns, + getRowId: (row) => row.id, + reactivity: { + column: true, + cell: true, + row: true, + header: true, + }, + })), + ) + } + + const table = createTestTable() + const tablePropertyKeys = Object.keys(table) + + describe('Table property reactivity', () => { + test.each( + tablePropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(table, property), + ]), + )('property (%s) is computed -> (%s)', (name, expected) => { + const tableProperty = table[name as keyof typeof table] + expect(isSignal(tableProperty)).toEqual(expected) + }) + + describe('will create a computed for non detectable computed properties', () => { + test('getIsSomeRowsPinned', () => { + table.getIsSomeRowsPinned('top') + table.getIsSomeRowsPinned('bottom') + table.getIsSomeRowsPinned() + + expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( + '["top"]', + ) + expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( + '["bottom"]', + ) + expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( + '[]', + ) + }) + }) + }) + + describe('Header property reactivity', () => { + const headers = table.getHeaderGroups() + headers.forEach((headerGroup, index) => { + const headerPropertyKeys = Object.keys(headerGroup) + test.each( + headerPropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(headerGroup, property), + ]), + )( + `HeaderGroup ${headerGroup.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = headerGroup[name as keyof typeof headerGroup] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + + const headers = headerGroup.headers + headers.forEach((header, cellIndex) => { + const headerPropertyKeys = Object.keys(header) + test.each( + headerPropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(header, property), + ]), + )( + `HeaderGroup ${headerGroup.id} (${index}) / Header ${header.id} - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = header[name as keyof typeof header] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + }) + + describe('Column property reactivity', () => { + const columns = table.getAllColumns() + columns.forEach((column, index) => { + const columnPropertyKeys = Object.keys(column) + test.each( + columnPropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(column, property), + ]), + )( + `Column ${column.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = column[name as keyof typeof column] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + + describe('Row property reactivity', () => { + const flatRows = table.getRowModel().flatRows + flatRows.forEach((row, index) => { + const rowsPropertyKeys = Object.keys(row) + test.each( + rowsPropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(row, property), + ]), + )( + `Row ${row.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = row[name as keyof typeof row] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + + const cells = row.getAllCells() + cells.forEach((cell, cellIndex) => { + const cellPropertyKeys = Object.keys(cell) + test.each( + cellPropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(cell, property), + ]), + )( + `Row ${row.id} (${index}) / Cell ${cell.id} - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = cell[name as keyof typeof cell] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + }) + + describe('Integration', () => { + test('methods works will be reactive effects', () => { + const data = signal>([{ id: '1', title: 'Title' }]) + const table = createTestTable(data) + const isSelectedRow1Captor = vi.fn<(val: boolean) => void>() + const cellGetValueCaptor = vi.fn<(val: unknown) => void>() + const columnIsVisibleCaptor = vi.fn<(val: boolean) => void>() + + // This will test a case where you put in the effect a single cell property method + // which will trigger effect reschedule only when the value changes, acting like + // its a computed value + const cell = computed( + () => table.getRowModel().rows[0]!.getAllCells()[0]!, + ) + + TestBed.runInInjectionContext(() => { + effect(() => { + isSelectedRow1Captor(cell().row.getIsSelected()) + }) + effect(() => { + cellGetValueCaptor(cell().getValue()) + }) + effect(() => { + columnIsVisibleCaptor(cell().column.getIsVisible()) + }) + }) + + TestBed.tick() + expect(isSelectedRow1Captor).toHaveBeenCalledTimes(1) + expect(cellGetValueCaptor).toHaveBeenCalledTimes(1) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1) + + cell().row.toggleSelected(true) + TestBed.tick() + expect(isSelectedRow1Captor).toHaveBeenCalledTimes(2) + expect(cellGetValueCaptor).toHaveBeenCalledTimes(1) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1) + + data.set([{ id: '1', title: 'Title 3' }]) + TestBed.tick() + // In this case it will be called twice since `data` will change and + // the cell instance will be recreated + expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3) + expect(cellGetValueCaptor).toHaveBeenCalledTimes(2) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2) + + cell().column.toggleVisibility(false) + TestBed.tick() + expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3) + expect(cellGetValueCaptor).toHaveBeenCalledTimes(2) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3) + + expect(isSelectedRow1Captor.mock.calls).toEqual([[false], [true], [true]]) + expect(cellGetValueCaptor.mock.calls).toEqual([['1'], ['1']]) + expect(columnIsVisibleCaptor.mock.calls).toEqual([ + [true], + [true], + [false], + ]) + }) + }) +}) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index 543fd87edb..1f74844c66 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -1,6 +1,12 @@ import { isProxy } from 'node:util/types' import { describe, expect, test, vi } from 'vitest' -import { Component, effect, input, isSignal, signal } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + effect, + input, + signal, +} from '@angular/core' import { TestBed } from '@angular/core/testing' import { ColumnDef, @@ -8,21 +14,17 @@ import { stockFeatures, } from '@tanstack/table-core' import { RowModel, injectTable } from '../src' -import { - getFnReactiveCache, - setFixtureSignalInputs, - testShouldBeComputedProperty, -} from './test-utils' import type { PaginationState } from '../src' describe('injectTable', () => { - test('should render with required signal inputs', () => { + test('should support required signal inputs', () => { @Component({ - selector: 'app-fake', + selector: 'app-table', template: ``, standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, }) - class FakeComponent { + class TableComponent { data = input.required>() table = injectTable(() => ({ @@ -32,12 +34,18 @@ describe('injectTable', () => { })) } - const fixture = TestBed.createComponent(FakeComponent) - setFixtureSignalInputs(fixture, { - data: [], + @Component({ + selector: 'app-root', + imports: [TableComponent], + template: ` `, + changeDetection: ChangeDetectionStrategy.OnPush, }) + class RootComponent {} + const fixture = TestBed.createComponent(RootComponent) fixture.detectChanges() + + fixture.whenRenderingDone() }) describe('Proxy table', () => { @@ -56,8 +64,8 @@ describe('injectTable', () => { })), ) - test('table must be a signal', () => { - expect(isSignal(table.get)).toEqual(true) + test('table is proxy', () => { + expect(isProxy(table)).toBe(true) }) test('supports "in" operator', () => { @@ -67,7 +75,7 @@ describe('injectTable', () => { }) test('supports "Object.keys"', () => { - const keys = Object.keys(table.get()) + const keys = Object.keys(table.get()).concat('state') expect(Object.keys(table)).toEqual(keys) }) @@ -123,170 +131,3 @@ describe('injectTable', () => { }) }) }) - -describe('injectTable - Experimental reactivity', () => { - type Data = { id: string; title: string } - const data = signal>([{ id: '1', title: 'Title' }]) - const columns: Array> = [ - { id: 'id', header: 'Id', cell: (context) => context.getValue() }, - { id: 'title', header: 'Title', cell: (context) => context.getValue() }, - ] - const table = TestBed.runInInjectionContext(() => - injectTable(() => ({ - data: data(), - _features: { ...stockFeatures }, - columns: columns, - getRowId: (row) => row.id, - reactivity: { - column: true, - cell: true, - row: true, - header: true, - }, - })), - ) - const tablePropertyKeys = Object.keys(table) - - describe('Proxy', () => { - test('table is proxy', () => { - expect(isProxy(table)).toBe(true) - }) - - test('supports "in" operator', () => { - expect('_features' in table).toBe(true) - expect('options' in table).toBe(true) - expect('notFound' in table).toBe(false) - }) - - test('supports "Object.keys"', () => { - const keys = Object.keys(table) - expect(Object.keys(table)).toEqual(keys) - }) - - test('supports "Object.has"', () => { - const keys = Object.keys(table) - expect(Object.keys(table)).toEqual(keys) - }) - }) - - describe('Table property reactivity', () => { - test.each( - tablePropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(table, property), - ]), - )('property (%s) is computed -> (%s)', (name, expected) => { - const tableProperty = table[name as keyof typeof table] - expect(isSignal(tableProperty)).toEqual(expected) - }) - - describe('will create a computed for non detectable computed properties', () => { - test('getIsSomeRowsPinned', () => { - table.getIsSomeRowsPinned('top') - table.getIsSomeRowsPinned('bottom') - table.getIsSomeRowsPinned() - - expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( - '["top"]', - ) - expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( - '["bottom"]', - ) - expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty( - '[]', - ) - }) - }) - }) - - describe('Header property reactivity', () => { - const headers = table.getHeaderGroups() - headers.forEach((headerGroup, index) => { - const headerPropertyKeys = Object.keys(headerGroup) - test.each( - headerPropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(headerGroup, property), - ]), - )( - `HeaderGroup ${headerGroup.id} (${index}) - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = headerGroup[name as keyof typeof headerGroup] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - - const headers = headerGroup.headers - headers.forEach((header, cellIndex) => { - const headerPropertyKeys = Object.keys(header) - test.each( - headerPropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(header, property), - ]), - )( - `HeaderGroup ${headerGroup.id} (${index}) / Header ${header.id} - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = header[name as keyof typeof header] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - }) - }) - }) - - describe('Column property reactivity', () => { - const columns = table.getAllColumns() - columns.forEach((column, index) => { - const columnPropertyKeys = Object.keys(column) - test.each( - columnPropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(column, property), - ]), - )( - `Column ${column.id} (${index}) - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = column[name as keyof typeof column] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - }) - }) - - describe('Row property reactivity', () => { - const flatRows = table.getRowModel().flatRows - flatRows.forEach((row, index) => { - const rowsPropertyKeys = Object.keys(row) - test.each( - rowsPropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(row, property), - ]), - )( - `Row ${row.id} (${index}) - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = row[name as keyof typeof row] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - - const cells = row.getAllCells() - cells.forEach((cell, cellIndex) => { - const cellPropertyKeys = Object.keys(cell) - test.each( - cellPropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(cell, property), - ]), - )( - `Row ${row.id} (${index}) / Cell ${cell.id} - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = cell[name as keyof typeof cell] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - }) - }) - }) -}) diff --git a/packages/angular-table/tests/test-utils.ts b/packages/angular-table/tests/test-utils.ts index 64f1eb2253..9ff7353aa1 100644 --- a/packages/angular-table/tests/test-utils.ts +++ b/packages/angular-table/tests/test-utils.ts @@ -83,6 +83,6 @@ export const testShouldBeComputedProperty = ( return false } -export function getFnReactiveCache(fn: any): void { +export function getFnReactiveCache(fn: any): any { return fn._reactiveCache } From 80ccaf59a835802bde23d6a099231e1f87cab3a7 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 10 Jan 2026 16:21:12 +0100 Subject: [PATCH 11/30] refactor(reactivity): enhance memoization and improve reactivity checks --- packages/angular-table/src/reactivityUtils.ts | 32 +++++++++++++------ .../tests/angularReactivityFeature.test.ts | 18 ++++++++--- packages/angular-table/tests/test-utils.ts | 17 +++++++++- packages/table-core/src/utils.ts | 26 +++++++++++---- 4 files changed, 72 insertions(+), 21 deletions(-) diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts index b9b1870b87..9545c4afc7 100644 --- a/packages/angular-table/src/reactivityUtils.ts +++ b/packages/angular-table/src/reactivityUtils.ts @@ -1,4 +1,6 @@ import { computed } from '@angular/core' +import { $internalMemoFnMeta, getMemoFnMeta } from '@tanstack/table-core' +import type { MemoFnMeta } from '@tanstack/table-core' import type { Signal } from '@angular/core' export const $TABLE_REACTIVE = Symbol('reactive') @@ -7,10 +9,8 @@ export function markReactive(obj: T): void { Object.defineProperty(obj, $TABLE_REACTIVE, { value: true }) } -export function isReactive( - obj: T, -): obj is T & { [$TABLE_REACTIVE]: true } { - return Reflect.get(obj, $TABLE_REACTIVE) === true +export function isReactive(obj: T): boolean { + return Reflect.get(obj as {}, $TABLE_REACTIVE) === true } /** @@ -151,7 +151,7 @@ function serializeArgs(...args: Array) { function getFnArgsLength( fn: ((...args: any) => any) & { originalArgsLength?: number }, ): number { - return Math.max(0, fn.originalArgsLength ?? fn.length) + return Math.max(0, getMemoFnMeta(fn)?.originalArgsLength ?? fn.length) } export function assignReactivePrototypeAPI( @@ -159,6 +159,8 @@ export function assignReactivePrototypeAPI( prototype: Record, fnName: string, ) { + if (isReactive(prototype[fnName])) return + const fn = prototype[fnName] const originalArgsLength = getFnArgsLength(fn) @@ -171,10 +173,18 @@ export function assignReactivePrototypeAPI( // Create a cache in the current prototype to allow the signals // to be garbage collected. Shorthand for a WeakMap implementation self._reactiveCache ??= {} - return (self._reactiveCache[`${self.id}${fnName}`] ??= computed(() => { - notifier() - return fn.apply(self) - }, {})) + const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= computed( + () => { + notifier() + return fn.apply(self) + }, + {}, + )) + markReactive(cached) + cached[$internalMemoFnMeta] = { + originalArgsLength, + } satisfies MemoFnMeta + return cached }, }) } else { @@ -182,5 +192,9 @@ export function assignReactivePrototypeAPI( notifier() return fn.apply(this, args) } + markReactive(prototype[fnName]) + prototype[fnName][$internalMemoFnMeta] = { + originalArgsLength, + } satisfies MemoFnMeta } } diff --git a/packages/angular-table/tests/angularReactivityFeature.test.ts b/packages/angular-table/tests/angularReactivityFeature.test.ts index bf7dacb891..f2de587be1 100644 --- a/packages/angular-table/tests/angularReactivityFeature.test.ts +++ b/packages/angular-table/tests/angularReactivityFeature.test.ts @@ -93,7 +93,9 @@ describe('angularReactivityFeature', () => { const headers = headerGroup.headers headers.forEach((header, cellIndex) => { - const headerPropertyKeys = Object.keys(header) + const headerPropertyKeys = Object.keys(header).concat( + Object.getOwnPropertyNames(Object.getPrototypeOf(header)), + ) test.each( headerPropertyKeys.map((property) => [ property, @@ -113,7 +115,9 @@ describe('angularReactivityFeature', () => { describe('Column property reactivity', () => { const columns = table.getAllColumns() columns.forEach((column, index) => { - const columnPropertyKeys = Object.keys(column) + const columnPropertyKeys = Object.keys(column).concat( + Object.getOwnPropertyNames(Object.getPrototypeOf(column)), + ) test.each( columnPropertyKeys.map((property) => [ property, @@ -129,10 +133,12 @@ describe('angularReactivityFeature', () => { }) }) - describe('Row property reactivity', () => { + describe('Row and cells property reactivity', () => { const flatRows = table.getRowModel().flatRows flatRows.forEach((row, index) => { - const rowsPropertyKeys = Object.keys(row) + const rowsPropertyKeys = Object.keys(row).concat( + Object.getOwnPropertyNames(Object.getPrototypeOf(row)), + ) test.each( rowsPropertyKeys.map((property) => [ property, @@ -148,7 +154,9 @@ describe('angularReactivityFeature', () => { const cells = row.getAllCells() cells.forEach((cell, cellIndex) => { - const cellPropertyKeys = Object.keys(cell) + const cellPropertyKeys = Object.keys(cell).concat( + Object.getOwnPropertyNames(Object.getPrototypeOf(cell)), + ) test.each( cellPropertyKeys.map((property) => [ property, diff --git a/packages/angular-table/tests/test-utils.ts b/packages/angular-table/tests/test-utils.ts index 9ff7353aa1..7681a4d4a7 100644 --- a/packages/angular-table/tests/test-utils.ts +++ b/packages/angular-table/tests/test-utils.ts @@ -1,4 +1,5 @@ import { SIGNAL } from '@angular/core/primitives/signals' +import { getMemoFnMeta } from '@tanstack/table-core' import type { InputSignal } from '@angular/core' import type { ComponentFixture } from '@angular/core/testing' @@ -59,11 +60,24 @@ const staticNonComputedProperties = [ 'getRowId', 'getRow', 'getIsSomeColumnsPinned', + 'getContext', ] + +function getFnArgsLength( + fn: ((...args: any) => any) & { originalArgsLength?: number }, +): number { + return Math.max(0, getMemoFnMeta(fn)?.originalArgsLength ?? fn.length) +} + export const testShouldBeComputedProperty = ( testObj: any, propertyName: string, + excludeComputed: Array = [], ) => { + if (excludeComputed.includes(propertyName)) { + return false + } + if (staticNonComputedProperties.some((prop) => propertyName === prop)) { return false } @@ -78,7 +92,8 @@ export const testShouldBeComputedProperty = ( // Only properties with no arguments are computed const fn = testObj[propertyName] // Cannot test if is lazy computed since we return the unwrapped value - return fn instanceof Function && fn.length === 0 + const args = Math.max(0, getFnArgsLength(fn) - 1) + return fn instanceof Function && args === 0 } return false } diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts index 3dca80a8d1..6d6f56be41 100755 --- a/packages/table-core/src/utils.ts +++ b/packages/table-core/src/utils.ts @@ -60,6 +60,23 @@ export function flattenBy( return flat } +export const $internalMemoFnMeta = Symbol('memoFnMeta') +export type MemoFnMeta = { originalArgsLength?: number } + +/** + * @internal + */ +function setMemoFnMeta(fn: Function, meta: MemoFnMeta) { + Object.defineProperty(fn, $internalMemoFnMeta, { value: meta }) +} + +/** + * @internal + */ +export function getMemoFnMeta(fn: any): MemoFnMeta | null { + return (typeof fn === 'function' && fn[$internalMemoFnMeta]) ?? null +} + interface MemoOptions, TDepArgs, TResult> { fn: (...args: NoInfer) => TResult memoDeps?: (depArgs?: TDepArgs) => [...TDeps] | undefined @@ -104,9 +121,7 @@ export const memo = , TDepArgs, TResult>({ return result } - Object.defineProperties(memoizedFn, { - originalArgsLength: { value: fn.length }, - }) + setMemoFnMeta(memoizedFn, { originalArgsLength: fn.length }) return memoizedFn } @@ -362,9 +377,8 @@ export function assignPrototypeAPIs< return fn(this, ...args) } } - Object.defineProperties(prototype[fnKey], { - originalArgsLength: { value: fn.length }, - }) + + setMemoFnMeta(prototype[fnKey], { originalArgsLength: fn.length }) } } From 8c8bf84a424062c5c90445a07e441975b9364127 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 12:37:40 +0100 Subject: [PATCH 12/30] add table cell contexts --- packages/angular-table/package.json | 6 +- packages/angular-table/src/context/cell.ts | 65 +++----------- .../angular-table/src/context/flex-render.ts | 87 +++++++++++++++++++ packages/angular-table/src/context/header.ts | 14 ++- packages/angular-table/src/context/table.ts | 14 ++- packages/angular-table/src/index.ts | 1 + 6 files changed, 127 insertions(+), 60 deletions(-) create mode 100644 packages/angular-table/src/context/flex-render.ts diff --git a/packages/angular-table/package.json b/packages/angular-table/package.json index 97d583fea8..8d62694b8a 100644 --- a/packages/angular-table/package.json +++ b/packages/angular-table/package.json @@ -33,7 +33,7 @@ } }, "engines": { - "node": ">=16" + "node": ">=18" }, "files": [ "dist", @@ -58,8 +58,8 @@ "devDependencies": { "@analogjs/vite-plugin-angular": "^2.2.1", "@analogjs/vitest-angular": "^2.2.1", - "@angular/core": "^21.0.6", - "@angular/platform-browser": "^21.0.6", + "@angular/core": "^21.0.0", + "@angular/platform-browser": "^21.0.0", "ng-packagr": "^21.0.1", "typescript": "5.9.3" }, diff --git a/packages/angular-table/src/context/cell.ts b/packages/angular-table/src/context/cell.ts index 864b39d0d6..f44e26f2f9 100644 --- a/packages/angular-table/src/context/cell.ts +++ b/packages/angular-table/src/context/cell.ts @@ -1,9 +1,6 @@ -import { Directive, effect, inject, input } from '@angular/core' +import { Directive, InjectionToken, inject, input } from '@angular/core' import { Cell, CellData, RowData, TableFeatures } from '@tanstack/table-core' -import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' -import { FlexRender } from '../flex-render' import type { Signal } from '@angular/core' -import type { Header } from '@tanstack/table-core' export interface TanStackTableCellContext< TFeatures extends TableFeatures, @@ -13,9 +10,19 @@ export interface TanStackTableCellContext< cell: Signal> } +export const CellContextToken = new InjectionToken< + TanStackTableCellContext['cell'] +>('[TanStack Table] CellContext') + @Directive({ selector: '[tanStackTableCell]', exportAs: 'cell', + providers: [ + { + provide: CellContextToken, + useFactory: () => inject(TanStackTableCell).cell, + }, + ], }) export class TanStackTableCell< TFeatures extends TableFeatures, @@ -32,53 +39,5 @@ export function injectTableCellContext< TData extends RowData, TValue extends CellData, >(): TanStackTableCellContext['cell'] { - return inject(TanStackTableCell).cell -} - -@Directive({ - selector: - 'ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]', - hostDirectives: [{ directive: FlexRender }], -}) -export class CellFlexRender< - TFeatures extends TableFeatures, - TData extends RowData, - TValue, -> { - readonly flexRender = inject(FlexRender) - - readonly cell = input>(undefined, { - alias: 'flexRenderCell', - }) - - readonly header = input>(undefined, { - alias: 'flexRenderHeader', - }) - - readonly footer = input>(undefined, { - alias: 'flexRenderFooter', - }) - - constructor() { - effect(() => { - const cell = this.cell() - const header = this.header() - const footer = this.footer() - const contentNode = this.flexRender.content[SIGNAL] - const propsNode = this.flexRender.props[SIGNAL] - - if (cell) { - signalSetFn(contentNode, cell.column.columnDef.cell) - signalSetFn(propsNode, cell.getContext()) - } - if (header) { - signalSetFn(contentNode, header.column.columnDef.header) - signalSetFn(propsNode, header.getContext()) - } - if (footer) { - signalSetFn(contentNode, footer.column.columnDef.footer) - signalSetFn(propsNode, footer.getContext()) - } - }) - } + return inject(CellContextToken) } diff --git a/packages/angular-table/src/context/flex-render.ts b/packages/angular-table/src/context/flex-render.ts new file mode 100644 index 0000000000..45ba4269ed --- /dev/null +++ b/packages/angular-table/src/context/flex-render.ts @@ -0,0 +1,87 @@ +import { Directive, effect, inject, input, untracked } from '@angular/core' +import { + Cell, + CellData, + Header, + RowData, + TableFeatures, +} from '@tanstack/table-core' +import { FlexRender } from '../flex-render' +import { CellContextToken } from './cell' +import { HeaderContextToken } from './header' +import { TableContextToken } from './table' + +@Directive({ + selector: + 'ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]', + hostDirectives: [{ directive: FlexRender }], +}) +export class CellFlexRender< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +> { + readonly #flexRender = inject(FlexRender) + + readonly cell = input>(undefined, { + alias: 'flexRenderCell', + }) + + readonly header = input>(undefined, { + alias: 'flexRenderHeader', + }) + + readonly footer = input>(undefined, { + alias: 'flexRenderFooter', + }) + + constructor() { + effect(() => { + const cell = this.cell() + const header = this.header() + const footer = this.footer() + const { content, props, staticProviders } = this.#flexRender + + if (cell) { + content.set(cell.column.columnDef.cell) + props.set(cell.getContext()) + // TODO: fix + untracked(() => + this.#flexRender.ngOnChanges({ + inputContent: { + currentValue: cell.column.columnDef.cell, + }, + inputProps: { + currentValue: cell.getContext(), + }, + } as any), + ) + staticProviders.set([ + { provide: TableContextToken, useValue: () => cell.table }, + { provide: CellContextToken, useValue: () => cell }, + ]) + } + + if (header) { + content.set(header.column.columnDef.header) + props.set(header.getContext()) + staticProviders.set([ + { provide: TableContextToken, useValue: () => header.table }, + { provide: HeaderContextToken, useValue: () => header }, + ]) + } + + if (footer) { + content.set(footer.column.columnDef.footer) + props.set(footer.getContext()) + staticProviders.set([ + { provide: TableContextToken, useValue: () => footer.table }, + { + provide: HeaderContextToken, + useValue: () => footer, + }, + ]) + } + }) + } +} diff --git a/packages/angular-table/src/context/header.ts b/packages/angular-table/src/context/header.ts index 61d2adf9b6..b36a7c9529 100644 --- a/packages/angular-table/src/context/header.ts +++ b/packages/angular-table/src/context/header.ts @@ -1,7 +1,11 @@ -import { Directive, inject, input } from '@angular/core' +import { Directive, InjectionToken, inject, input } from '@angular/core' import { CellData, Header, RowData, TableFeatures } from '@tanstack/table-core' import type { Signal } from '@angular/core' +export const HeaderContextToken = new InjectionToken< + TanStackTableHeaderContext['header'] +>('[TanStack Table] HeaderContext') + export interface TanStackTableHeaderContext< TFeatures extends TableFeatures, TData extends RowData, @@ -13,6 +17,12 @@ export interface TanStackTableHeaderContext< @Directive({ selector: '[tanStackTableHeader]', exportAs: 'header', + providers: [ + { + provide: HeaderContextToken, + useFactory: () => inject(TanStackTableHeader).header, + }, + ], }) export class TanStackTableHeader< TFeatures extends TableFeatures, @@ -29,5 +39,5 @@ export function injectTableHeaderContext< TData extends RowData, TValue extends CellData, >(): TanStackTableHeaderContext['header'] { - return inject(TanStackTableHeader).header + return inject(HeaderContextToken) } diff --git a/packages/angular-table/src/context/table.ts b/packages/angular-table/src/context/table.ts index 9f6cdb9421..1a74ff231c 100644 --- a/packages/angular-table/src/context/table.ts +++ b/packages/angular-table/src/context/table.ts @@ -1,7 +1,11 @@ -import { Directive, inject, input } from '@angular/core' +import { Directive, InjectionToken, inject, input } from '@angular/core' import { RowData, Table, TableFeatures } from '@tanstack/table-core' import type { Signal } from '@angular/core' +export const TableContextToken = new InjectionToken< + TanStackTableContext['table'] +>('[TanStack Table] HeaderContext') + export interface TanStackTableContext< TFeatures extends TableFeatures, TData extends RowData, @@ -12,6 +16,12 @@ export interface TanStackTableContext< @Directive({ selector: '[tanStackTable]', exportAs: 'table', + providers: [ + { + provide: TableContextToken, + useFactory: () => inject(TanStackTable).table, + }, + ], }) export class TanStackTable< TFeatures extends TableFeatures, @@ -26,5 +36,5 @@ export function injectTableContext< TFeatures extends TableFeatures, TData extends RowData, >(): TanStackTableContext['table'] { - return inject(TanStackTable).table + return inject(TableContextToken) } diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index 52cd4133e1..738363b515 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -12,3 +12,4 @@ export * from './context/cell' export * from './context/header' export * from './context/table' export * from './context/createTableHook' +export * from './context/flex-render' From 9c92b40484df59d42857daab41b6ad65f681900d Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 13:09:05 +0100 Subject: [PATCH 13/30] cleanup --- packages/angular-table/src/flex-render.ts | 89 ++++++++++++------- .../flex-render/flex-render-component-ref.ts | 7 +- pnpm-lock.yaml | 4 +- 3 files changed, 65 insertions(+), 35 deletions(-) diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index 143dcbcebd..47d2d83f54 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -5,6 +5,7 @@ import { EffectRef, Injector, OnChanges, + Provider, SimpleChanges, TemplateRef, Type, @@ -13,6 +14,7 @@ import { effect, inject, input, + linkedSignal, runInInjectionContext, } from '@angular/core' import { FlexRenderComponentProps } from './flex-render/context' @@ -28,10 +30,13 @@ import { FlexRenderView, mapToFlexRenderTypedContent, } from './flex-render/view' +import type { InputSignal } from '@angular/core' import type { FlexRenderTypedContent } from './flex-render/view' import type { CellContext, + CellData, HeaderContext, + RowData, Table, TableFeatures, } from '@tanstack/table-core' @@ -54,48 +59,60 @@ export type FlexRenderContent> = @Directive({ selector: 'ng-template[flexRender]', standalone: true, - providers: [FlexRenderComponentFactory], }) export class FlexRender< + TFeatures extends TableFeatures, + TRowData extends RowData, + TValue extends CellData, TProps extends | NonNullable - | CellContext - | HeaderContext, + | CellContext + | HeaderContext, > - implements OnChanges, DoCheck + implements DoCheck, OnChanges { - readonly #flexRenderComponentFactory = inject(FlexRenderComponentFactory) + readonly #flexRenderComponentFactory = new FlexRenderComponentFactory( + inject(ViewContainerRef), + ) readonly #changeDetectorRef = inject(ChangeDetectorRef) - readonly content = input< + readonly inputContent: InputSignal< | number | string | ((props: TProps) => FlexRenderContent) | null | undefined - | any - >(undefined, { + > = input(undefined as any, { alias: 'flexRender', }) + readonly content = linkedSignal(() => this.inputContent()) - readonly props = input({} as TProps, { + readonly inputProps = input({} as TProps, { alias: 'flexRenderProps', }) + readonly props = linkedSignal(() => this.inputProps()) - readonly notifier = input<'doCheck' | 'tableChange'>('doCheck', { + readonly inputNotifier = input<'doCheck' | 'tableChange'>('doCheck', { alias: 'flexRenderNotifier', }) + readonly notifier = linkedSignal(() => this.inputNotifier()) readonly #injector = inject(Injector) - readonly injector = input(this.#injector, { + readonly inputInjector = input(this.#injector, { alias: 'flexRenderInjector', }) + readonly injector = linkedSignal(() => this.inputInjector()) + + readonly inputStaticProviders = input>([], { + alias: 'flexRenderStaticProviders', + }) + readonly staticProviders = linkedSignal(() => this.inputStaticProviders()) readonly viewContainerRef = inject(ViewContainerRef) readonly templateRef = inject(TemplateRef) - table: Table + table: Table | null = null renderFlags = FlexRenderFlags.ViewFirstRender renderView: FlexRenderView | null = null @@ -114,14 +131,16 @@ export class FlexRender< return mapToFlexRenderTypedContent(latestContent) }) - ngOnChanges(changes: SimpleChanges>) { - if (changes.props) { - const props = changes.props.currentValue + ngOnChanges( + changes: SimpleChanges>, + ) { + if (changes.inputProps) { + const props = changes.inputProps.currentValue this.table = 'table' in props ? props.table : null this.renderFlags |= FlexRenderFlags.PropsReferenceChanged this.bindTableDirtyCheck() } - if (changes.content) { + if (changes.inputContent) { this.renderFlags |= FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender this.update() @@ -163,9 +182,10 @@ export class FlexRender< this.#tableChangeEffect = null let firstCheck = !!(this.renderFlags & FlexRenderFlags.ViewFirstRender) if (this.table && this.notifier() === 'tableChange') { + const table = this.table this.#tableChangeEffect = effect( () => { - this.table.get() + table.get() if (firstCheck) { firstCheck = false return @@ -307,16 +327,8 @@ export class FlexRender< { kind: 'flexRenderComponent' } >, ): FlexRenderComponentView { - const { inputs, outputs, injector } = flexRenderComponent.content - - const getContext = () => this.props() - const proxy = new Proxy(this.props(), { - get: (_, key) => getContext()[key as keyof typeof _], - }) - const componentInjector = Injector.create({ - parent: injector ?? this.injector(), - providers: [{ provide: FlexRenderComponentProps, useValue: proxy }], - }) + const { injector } = flexRenderComponent.content + const componentInjector = this.#getComponentInjector(injector) const view = this.#flexRenderComponentFactory.createComponent( flexRenderComponent.content, componentInjector, @@ -327,15 +339,30 @@ export class FlexRender< #renderCustomComponent( component: Extract, ): FlexRenderComponentView { + const instance = flexRenderComponent(component.content, { + inputs: this.props(), + injector: this.#getComponentInjector(this.injector()), + }) const view = this.#flexRenderComponentFactory.createComponent( - flexRenderComponent(component.content, { - inputs: this.props(), - injector: this.injector(), - }), + instance, this.injector(), ) return new FlexRenderComponentView(component, view) } + + #getComponentInjector(parentInjector?: Injector) { + const getContext = () => this.props() + const proxy = new Proxy(this.props(), { + get: (_, key) => getContext()[key as keyof typeof _], + }) + return Injector.create({ + parent: parentInjector ?? this.injector(), + providers: [ + ...this.staticProviders(), + { provide: FlexRenderComponentProps, useValue: proxy }, + ], + }) + } } /** diff --git a/packages/angular-table/src/flex-render/flex-render-component-ref.ts b/packages/angular-table/src/flex-render/flex-render-component-ref.ts index 8d115dfcba..34c09b6031 100644 --- a/packages/angular-table/src/flex-render/flex-render-component-ref.ts +++ b/packages/angular-table/src/flex-render/flex-render-component-ref.ts @@ -8,13 +8,16 @@ import { OutputEmitterRef, OutputRefSubscription, ViewContainerRef, - inject, } from '@angular/core' import { FlexRenderComponent } from './flex-render-component' @Injectable() export class FlexRenderComponentFactory { - #viewContainerRef = inject(ViewContainerRef) + readonly #viewContainerRef: ViewContainerRef + + constructor(viewContainerRef: ViewContainerRef) { + this.#viewContainerRef = viewContainerRef + } createComponent( flexRenderComponent: FlexRenderComponent, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9fa59ccc9..464565dffb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3494,10 +3494,10 @@ importers: specifier: ^2.2.1 version: 2.2.1(@analogjs/vite-plugin-angular@2.2.1(@angular-devkit/build-angular@21.0.4(u5fs7psindzucntcdedavjbwtm))(@angular/build@21.0.4(kc35yzw5n5t7efydd2g6bmpsfy)))(@angular-devkit/architect@0.2100.4(chokidar@4.0.3))(vitest@4.0.16(@types/node@25.0.3)(jiti@2.6.1)(jsdom@27.4.0(postcss@8.5.6))(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.6.1)) '@angular/core': - specifier: ^21.0.6 + specifier: ^21.0.0 version: 21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0) '@angular/platform-browser': - specifier: ^21.0.6 + specifier: ^21.0.0 version: 21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)) ng-packagr: specifier: ^21.0.1 From a024ecc6154e9f43992d11f377a74338c2af9d46 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 20:31:30 +0100 Subject: [PATCH 14/30] fixes some examples --- .../column-ordering/src/app/app.component.ts | 1 - .../src/app/app.component.ts | 1 - .../column-pinning/src/app/app.component.ts | 1 - .../src/app/app.component.ts | 40 ++++++++++--------- .../src/app/app.component.html | 2 +- .../expanding/src/app/app.component.ts | 5 ++- .../angular/filters/src/app/app.component.ts | 1 - .../angular/grouping/src/app/app.component.ts | 36 +---------------- examples/angular/grouping/src/app/columns.ts | 35 +++++++++++++++- .../remote-data/src/app/app.component.ts | 8 ++-- .../remote-data/src/app/app.config.server.ts | 5 +-- .../remote-data/src/app/app.routes.server.ts | 3 +- examples/angular/remote-data/src/server.ts | 6 +-- examples/angular/remote-data/tsconfig.json | 2 +- .../src/app/app.component.ts | 1 - .../row-selection/src/app/app.component.ts | 24 ++++++++--- .../src/app/selection-column.component.ts | 25 ++++++++---- .../person-table/person-table.component.ts | 1 - 18 files changed, 107 insertions(+), 90 deletions(-) diff --git a/examples/angular/column-ordering/src/app/app.component.ts b/examples/angular/column-ordering/src/app/app.component.ts index 09918b53e8..912bc682ee 100644 --- a/examples/angular/column-ordering/src/app/app.component.ts +++ b/examples/angular/column-ordering/src/app/app.component.ts @@ -96,7 +96,6 @@ export class AppComponent { columnOrder: this.columnOrder(), columnVisibility: this.columnVisibility(), }, - enableExperimentalReactivity: true, onColumnVisibilityChange: (updaterOrValue) => { typeof updaterOrValue === 'function' ? this.columnVisibility.update(updaterOrValue) diff --git a/examples/angular/column-pinning-sticky/src/app/app.component.ts b/examples/angular/column-pinning-sticky/src/app/app.component.ts index f3a6a4cb58..eb5a4a0610 100644 --- a/examples/angular/column-pinning-sticky/src/app/app.component.ts +++ b/examples/angular/column-pinning-sticky/src/app/app.component.ts @@ -104,7 +104,6 @@ export class AppComponent { _features, columns: this.columns(), data: this.data(), - enableExperimentalReactivity: true, debugTable: true, debugHeaders: true, debugColumns: true, diff --git a/examples/angular/column-pinning/src/app/app.component.ts b/examples/angular/column-pinning/src/app/app.component.ts index 83cebb4cb2..dca59189ee 100644 --- a/examples/angular/column-pinning/src/app/app.component.ts +++ b/examples/angular/column-pinning/src/app/app.component.ts @@ -114,7 +114,6 @@ export class AppComponent { columnOrder: this.columnOrder(), columnPinning: this.columnPinning(), }, - enableExperimentalReactivity: true, onColumnVisibilityChange: (updaterOrValue) => { typeof updaterOrValue === 'function' ? this.columnVisibility.update(updaterOrValue) diff --git a/examples/angular/column-resizing-performant/src/app/app.component.ts b/examples/angular/column-resizing-performant/src/app/app.component.ts index 90ecac95bd..0c59cc3888 100644 --- a/examples/angular/column-resizing-performant/src/app/app.component.ts +++ b/examples/angular/column-resizing-performant/src/app/app.component.ts @@ -5,17 +5,17 @@ import { signal, untracked, } from '@angular/core' -import type { ColumnDef, ColumnResizeMode } from '@tanstack/angular-table' import { + FlexRenderDirective, columnResizingFeature, columnSizingFeature, - FlexRenderDirective, injectTable, tableFeatures, } from '@tanstack/angular-table' -import type { Person } from './makeData' import { makeData } from './makeData' import { TableResizableCell, TableResizableHeader } from './resizable-cell' +import type { Person } from './makeData' +import type { ColumnDef, ColumnResizeMode } from '@tanstack/angular-table' const _features = tableFeatures({ columnSizingFeature, @@ -78,7 +78,23 @@ const defaultColumns: Array> = [ export class AppComponent { readonly data = signal>(makeData(200)) - readonly columnSizing = computed(() => this.table.store.state.columnSizing) + readonly table = injectTable(() => ({ + data: this.data(), + _features, + columns: defaultColumns, + columnResizeMode: 'onChange' as ColumnResizeMode, + defaultColumn: { + minSize: 60, + maxSize: 800, + }, + debugTable: true, + debugHeaders: true, + debugColumns: true, + })) + + readonly columnSizing = this.table.Subscribe({ + selector: (state) => state.columnSizing, + }) /** * Instead of calling `column.getSize()` on every render for every header @@ -99,24 +115,10 @@ export class AppComponent { return colSizes }) - readonly table = injectTable(() => ({ - data: this.data(), - _features, - columns: defaultColumns, - columnResizeMode: 'onChange' as ColumnResizeMode, - defaultColumn: { - minSize: 60, - maxSize: 800, - }, - debugTable: true, - debugHeaders: true, - debugColumns: true, - })) - readonly columnSizingDebugInfo = computed(() => JSON.stringify( { - columnSizing: this.table.store.state.columnSizing, + columnSizing: this.columnSizing(), }, null, 2, diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index 50ae2b1806..ca4394c6c2 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -56,5 +56,5 @@
- + diff --git a/examples/angular/expanding/src/app/app.component.ts b/examples/angular/expanding/src/app/app.component.ts index e8026047b2..059a55f965 100644 --- a/examples/angular/expanding/src/app/app.component.ts +++ b/examples/angular/expanding/src/app/app.component.ts @@ -105,8 +105,11 @@ export class AppComponent { JSON.stringify(this.expanded(), undefined, 2), ) + readonly rowSelectionState = this.table.Subscribe({ + selector: (state) => state.rowSelection, + }) readonly rawRowSelectionState = computed(() => - JSON.stringify(this.table.store.state.rowSelection, undefined, 2), + JSON.stringify(this.rowSelectionState(), undefined, 2), ) onPageInputChange(event: Event): void { diff --git a/examples/angular/filters/src/app/app.component.ts b/examples/angular/filters/src/app/app.component.ts index 1696f292af..23af7fdfe4 100644 --- a/examples/angular/filters/src/app/app.component.ts +++ b/examples/angular/filters/src/app/app.component.ts @@ -101,7 +101,6 @@ export class AppComponent { paginatedRowModel: createPaginatedRowModel(), sortedRowModel: createSortedRowModel(sortFns), }, - // enableExperimentalReactivity: true, columns: this.columns, data: this.data(), state: { diff --git a/examples/angular/grouping/src/app/app.component.ts b/examples/angular/grouping/src/app/app.component.ts index eedc6b5692..c04dc60a9e 100644 --- a/examples/angular/grouping/src/app/app.component.ts +++ b/examples/angular/grouping/src/app/app.component.ts @@ -5,42 +5,11 @@ import { computed, signal, } from '@angular/core' -import { - FlexRenderDirective, - aggregationFns, - columnFilteringFeature, - columnGroupingFeature, - createExpandedRowModel, - createFilteredRowModel, - createGroupedRowModel, - createPaginatedRowModel, - createTableHelper, - filterFns, - isFunction, - rowExpandingFeature, - rowPaginationFeature, -} from '@tanstack/angular-table' -import { columns } from './columns' +import { FlexRenderDirective, isFunction } from '@tanstack/angular-table' +import { columns, tableHelper } from './columns' import { makeData } from './makeData' -import type { Person } from './makeData' import type { GroupingState, Updater } from '@tanstack/angular-table' -export const tableHelper = createTableHelper({ - _features: { - columnGroupingFeature, - rowPaginationFeature, - columnFilteringFeature, - rowExpandingFeature, - }, - _rowModels: { - groupedRowModel: createGroupedRowModel(aggregationFns), - expandedRowModel: createExpandedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - filteredRowModel: createFilteredRowModel(filterFns), - }, - TData: {} as Person, -}) - @Component({ selector: 'app-root', standalone: true, @@ -58,7 +27,6 @@ export class AppComponent { ) readonly table = tableHelper.injectTable(() => ({ - enableExperimentalReactivity: true, data: this.data(), columns: columns, initialState: { diff --git a/examples/angular/grouping/src/app/columns.ts b/examples/angular/grouping/src/app/columns.ts index ae74a4c5bb..4bae2676d7 100644 --- a/examples/angular/grouping/src/app/columns.ts +++ b/examples/angular/grouping/src/app/columns.ts @@ -1,6 +1,37 @@ -import { tableHelper } from './app.component' +import { + aggregationFns, + columnFilteringFeature, + columnGroupingFeature, + createExpandedRowModel, + createFilteredRowModel, + createGroupedRowModel, + createPaginatedRowModel, + createTableHelper, + filterFns, + rowExpandingFeature, + rowPaginationFeature, +} from '@tanstack/angular-table' +import type { Person } from './makeData' -const { columnHelper } = tableHelper +export const tableHelper = createTableHelper({ + _features: { + columnGroupingFeature, + rowPaginationFeature, + columnFilteringFeature, + rowExpandingFeature, + }, + _rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filteredRowModel: createFilteredRowModel(filterFns), + }, + TData: {} as Person, +}) +export default tableHelper + +const { createColumnHelper } = tableHelper +const columnHelper = createColumnHelper() export const columns = columnHelper.columns([ columnHelper.group({ diff --git a/examples/angular/remote-data/src/app/app.component.ts b/examples/angular/remote-data/src/app/app.component.ts index 2cb507fcd1..8de00c8c8e 100644 --- a/examples/angular/remote-data/src/app/app.component.ts +++ b/examples/angular/remote-data/src/app/app.component.ts @@ -2,7 +2,6 @@ import { HttpParams } from '@angular/common/http' import { ChangeDetectionStrategy, Component, - ResourceStatus, linkedSignal, resource, signal, @@ -60,12 +59,12 @@ export class AppComponent { readonly sorting = signal([{ id: 'id', desc: false }]) readonly globalFilter = signal(null) readonly data = resource({ - request: () => ({ + params: () => ({ page: this.pagination(), globalFilter: this.globalFilter(), sorting: this.sorting(), }), - loader: ({ request: { page, globalFilter, sorting }, abortSignal }) => { + loader: ({ params: { page, globalFilter, sorting }, abortSignal }) => { let httpParams = new HttpParams({ fromObject: { _page: page.pageIndex + 1, @@ -127,8 +126,7 @@ export class AppComponent { status: this.data.status(), }), computation: (source, previous) => { - if (previous && source.status === ResourceStatus.Loading) - return previous.value + if (previous && source.status === 'loading') return previous.value return source.value ?? { items: [], totalCount: 0 } }, }) diff --git a/examples/angular/remote-data/src/app/app.config.server.ts b/examples/angular/remote-data/src/app/app.config.server.ts index 0893630247..0b2c82509c 100644 --- a/examples/angular/remote-data/src/app/app.config.server.ts +++ b/examples/angular/remote-data/src/app/app.config.server.ts @@ -1,12 +1,11 @@ import { mergeApplicationConfig } from '@angular/core' -import { provideServerRendering } from '@angular/platform-server' -import { provideServerRouting } from '@angular/ssr' +import { provideServerRendering, withRoutes } from '@angular/ssr' import { appConfig } from './app.config' import { serverRoutes } from './app.routes.server' import type { ApplicationConfig } from '@angular/core' const serverConfig: ApplicationConfig = { - providers: [provideServerRendering(), provideServerRouting(serverRoutes)], + providers: [provideServerRendering(withRoutes(serverRoutes))], } export const config = mergeApplicationConfig(appConfig, serverConfig) diff --git a/examples/angular/remote-data/src/app/app.routes.server.ts b/examples/angular/remote-data/src/app/app.routes.server.ts index dddd01e7df..09fcfa3cc9 100644 --- a/examples/angular/remote-data/src/app/app.routes.server.ts +++ b/examples/angular/remote-data/src/app/app.routes.server.ts @@ -1,4 +1,5 @@ -import { RenderMode, type ServerRoute } from '@angular/ssr' +import { RenderMode } from '@angular/ssr' +import type { ServerRoute } from '@angular/ssr' export const serverRoutes: Array = [ { diff --git a/examples/angular/remote-data/src/server.ts b/examples/angular/remote-data/src/server.ts index fac11ed3aa..b669f5c878 100644 --- a/examples/angular/remote-data/src/server.ts +++ b/examples/angular/remote-data/src/server.ts @@ -1,3 +1,5 @@ +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { AngularNodeAppEngine, createNodeRequestHandler, @@ -5,8 +7,6 @@ import { writeResponseToNodeResponse, } from '@angular/ssr/node' import express from 'express' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' const serverDistFolder = dirname(fileURLToPath(import.meta.url)) const browserDistFolder = resolve(serverDistFolder, '../browser') @@ -40,7 +40,7 @@ app.use( /** * Handle all other requests by rendering the Angular application. */ -app.use('/**', (req, res, next) => { +app.use('*', (req, res, next) => { angularApp .handle(req) .then((response) => diff --git a/examples/angular/remote-data/tsconfig.json b/examples/angular/remote-data/tsconfig.json index b58d3efc71..eb0dd3b6d5 100644 --- a/examples/angular/remote-data/tsconfig.json +++ b/examples/angular/remote-data/tsconfig.json @@ -15,7 +15,7 @@ "sourceMap": true, "declaration": false, "experimentalDecorators": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", diff --git a/examples/angular/row-selection-signal/src/app/app.component.ts b/examples/angular/row-selection-signal/src/app/app.component.ts index 18c7af0700..8791072cf2 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.ts +++ b/examples/angular/row-selection-signal/src/app/app.component.ts @@ -115,7 +115,6 @@ export class AppComponent { }, columns: this.columns, data: this.data(), - enableExperimentalReactivity: true, state: { rowSelection: this.rowSelection(), }, diff --git a/examples/angular/row-selection/src/app/app.component.ts b/examples/angular/row-selection/src/app/app.component.ts index bfcc77b615..0b896ebd79 100644 --- a/examples/angular/row-selection/src/app/app.component.ts +++ b/examples/angular/row-selection/src/app/app.component.ts @@ -6,6 +6,7 @@ import { viewChild, } from '@angular/core' import { + CellFlexRender, FlexRenderDirective, columnFilteringFeature, createFilteredRowModel, @@ -23,9 +24,9 @@ import { TableHeadSelectionComponent, TableRowSelectionComponent, } from './selection-column.component' +import type { TemplateRef } from '@angular/core' import type { Person } from './makeData' import type { ColumnDef, RowSelectionState } from '@tanstack/angular-table' -import type { TemplateRef } from '@angular/core' const tableHelper = createTableHelper({ _features: { @@ -43,7 +44,7 @@ const tableHelper = createTableHelper({ @Component({ selector: 'app-root', standalone: true, - imports: [FilterComponent, FlexRenderDirective, FormsModule], + imports: [FilterComponent, FlexRenderDirective, CellFlexRender, FormsModule], templateUrl: './app.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -52,6 +53,8 @@ export class AppComponent { readonly globalFilter = signal('') readonly data = signal(makeData(10_000)) + readonly myValue = signal(0) + readonly ageHeaderCell = viewChild.required>('ageHeaderCell') @@ -59,7 +62,7 @@ export class AppComponent { { id: 'select', header: () => { - return flexRenderComponent(TableHeadSelectionComponent) + const data = flexRenderComponent(TableHeadSelectionComponent) }, cell: () => { return flexRenderComponent(TableRowSelectionComponent) @@ -73,12 +76,14 @@ export class AppComponent { accessorKey: 'firstName', cell: (info) => info.getValue(), footer: (props) => props.column.id, - header: 'First name', + header: (props) => `${this.myValue()} First name`, }, { accessorFn: (row) => row.lastName, id: 'lastName', - cell: (info) => info.getValue(), + cell: (info) => { + return `lastname: ${info.getValue()} - is selected: ${info.row.getIsSelected()}` + }, header: () => 'Last Name', footer: (props) => props.column.id, }, @@ -123,7 +128,6 @@ export class AppComponent { state: { rowSelection: this.rowSelection(), }, - enableExperimentalReactivity: true, enableRowSelection: true, // enable row selection for all rows // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row onRowSelectionChange: (updaterOrValue) => { @@ -163,4 +167,12 @@ export class AppComponent { refreshData(): void { this.data.set(makeData(10_000)) } + + constructor() { + Reflect.set(window, 'updateValue', () => this.willUpdate()) + } + + willUpdate() { + this.myValue.update((x) => x + 1) + } } diff --git a/examples/angular/row-selection/src/app/selection-column.component.ts b/examples/angular/row-selection/src/app/selection-column.component.ts index 1ed10d8f1c..d3ac18aeaa 100644 --- a/examples/angular/row-selection/src/app/selection-column.component.ts +++ b/examples/angular/row-selection/src/app/selection-column.component.ts @@ -1,6 +1,13 @@ -import { injectFlexRenderContext } from '@tanstack/angular-table' -import { ChangeDetectionStrategy, Component } from '@angular/core' -import type { CellContext, HeaderContext } from '@tanstack/angular-table' +import { + injectFlexRenderContext, + injectTableCellContext, +} from '@tanstack/angular-table' +import { ChangeDetectionStrategy, Component, computed } from '@angular/core' +import type { + CellContext, + HeaderContext, + RowData, +} from '@tanstack/angular-table' @Component({ template: ` @@ -17,11 +24,11 @@ import type { CellContext, HeaderContext } from '@tanstack/angular-table' standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TableHeadSelectionComponent { - context = injectFlexRenderContext< - // @ts-expect-error TODO: Should fix types - HeaderContext<{ rowSelectionFeature: {} }, T, unknown> - >() +export class TableHeadSelectionComponent { + context = + injectFlexRenderContext< + HeaderContext<{ rowSelectionFeature: {} }, T, unknown> + >() } @Component({ @@ -39,6 +46,8 @@ export class TableHeadSelectionComponent { changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableRowSelectionComponent { + readonly cell = injectTableCellContext() + readonly row = computed(() => this.cell().row) context = // @ts-expect-error TODO: Should fix types injectFlexRenderContext>() diff --git a/examples/angular/signal-input/src/app/person-table/person-table.component.ts b/examples/angular/signal-input/src/app/person-table/person-table.component.ts index e06d8a00d9..8fb41632af 100644 --- a/examples/angular/signal-input/src/app/person-table/person-table.component.ts +++ b/examples/angular/signal-input/src/app/person-table/person-table.component.ts @@ -51,7 +51,6 @@ export class PersonTableComponent { state: { pagination: this.pagination(), }, - enableExperimentalReactivity: true, onPaginationChange: (updaterOrValue) => { typeof updaterOrValue === 'function' ? this.pagination.update(updaterOrValue) From a48875779ff750cdc97b47db49e7bcf7a703b188 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 20:32:17 +0100 Subject: [PATCH 15/30] refactor flexRender to work with signals --- .../src/angularReactivityFeature.ts | 1 - .../angular-table/src/context/flex-render.ts | 13 +- packages/angular-table/src/flex-render.ts | 175 +++++++++--------- 3 files changed, 93 insertions(+), 96 deletions(-) diff --git a/packages/angular-table/src/angularReactivityFeature.ts b/packages/angular-table/src/angularReactivityFeature.ts index ffbbd4e54f..f43ff19ece 100644 --- a/packages/angular-table/src/angularReactivityFeature.ts +++ b/packages/angular-table/src/angularReactivityFeature.ts @@ -30,7 +30,6 @@ export interface AngularReactivityFlags { } interface TableOptions_AngularReactivity { - enableExperimentalReactivity?: boolean reactivity?: Partial } diff --git a/packages/angular-table/src/context/flex-render.ts b/packages/angular-table/src/context/flex-render.ts index 45ba4269ed..af7408842f 100644 --- a/packages/angular-table/src/context/flex-render.ts +++ b/packages/angular-table/src/context/flex-render.ts @@ -1,4 +1,4 @@ -import { Directive, effect, inject, input, untracked } from '@angular/core' +import { Directive, effect, inject, input } from '@angular/core' import { Cell, CellData, @@ -45,17 +45,6 @@ export class CellFlexRender< if (cell) { content.set(cell.column.columnDef.cell) props.set(cell.getContext()) - // TODO: fix - untracked(() => - this.#flexRender.ngOnChanges({ - inputContent: { - currentValue: cell.column.columnDef.cell, - }, - inputProps: { - currentValue: cell.getContext(), - }, - } as any), - ) staticProviders.set([ { provide: TableContextToken, useValue: () => cell.table }, { provide: CellContextToken, useValue: () => cell }, diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index 47d2d83f54..70a21dbbe7 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -1,12 +1,9 @@ import { - ChangeDetectorRef, + DestroyRef, Directive, - DoCheck, EffectRef, Injector, - OnChanges, Provider, - SimpleChanges, TemplateRef, Type, ViewContainerRef, @@ -16,6 +13,7 @@ import { input, linkedSignal, runInInjectionContext, + untracked, } from '@angular/core' import { FlexRenderComponentProps } from './flex-render/context' import { FlexRenderFlags } from './flex-render/flags' @@ -30,6 +28,9 @@ import { FlexRenderView, mapToFlexRenderTypedContent, } from './flex-render/view' +import { CellContextToken } from './context/cell' +import { HeaderContextToken } from './context/header' +import { TableContextToken } from './context/table' import type { InputSignal } from '@angular/core' import type { FlexRenderTypedContent } from './flex-render/view' import type { @@ -56,6 +57,13 @@ export type FlexRenderContent> = | Record | undefined +export type FlexRenderInputContent> = + | number + | string + | ((props: TProps) => FlexRenderContent) + | null + | undefined + @Directive({ selector: 'ng-template[flexRender]', standalone: true, @@ -68,23 +76,17 @@ export class FlexRender< | NonNullable | CellContext | HeaderContext, -> - implements DoCheck, OnChanges -{ +> { readonly #flexRenderComponentFactory = new FlexRenderComponentFactory( inject(ViewContainerRef), ) - readonly #changeDetectorRef = inject(ChangeDetectorRef) - - readonly inputContent: InputSignal< - | number - | string - | ((props: TProps) => FlexRenderContent) - | null - | undefined - > = input(undefined as any, { - alias: 'flexRender', - }) + + readonly inputContent: InputSignal> = input( + undefined as FlexRenderInputContent, + { + alias: 'flexRender', + }, + ) readonly content = linkedSignal(() => this.inputContent()) readonly inputProps = input({} as TProps, { @@ -92,11 +94,6 @@ export class FlexRender< }) readonly props = linkedSignal(() => this.inputProps()) - readonly inputNotifier = input<'doCheck' | 'tableChange'>('doCheck', { - alias: 'flexRenderNotifier', - }) - readonly notifier = linkedSignal(() => this.inputNotifier()) - readonly #injector = inject(Injector) readonly inputInjector = input(this.#injector, { alias: 'flexRenderInjector', @@ -131,34 +128,40 @@ export class FlexRender< return mapToFlexRenderTypedContent(latestContent) }) - ngOnChanges( - changes: SimpleChanges>, - ) { - if (changes.inputProps) { - const props = changes.inputProps.currentValue - this.table = 'table' in props ? props.table : null - this.renderFlags |= FlexRenderFlags.PropsReferenceChanged - this.bindTableDirtyCheck() - } - if (changes.inputContent) { - this.renderFlags |= - FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender - this.update() - } - } + constructor() { + let previousContent: FlexRenderInputContent + let previousProps: TProps - ngDoCheck(): void { - if (this.renderFlags & FlexRenderFlags.ViewFirstRender) { - // On the initial render, the view is created during the `ngOnChanges` hook. - // Since `ngDoCheck` is called immediately afterward, there's no need to check for changes in this phase. - this.renderFlags &= ~FlexRenderFlags.ViewFirstRender - return - } + effect(() => { + const props = this.props() + const content = this.content() - if (this.notifier() === 'doCheck') { - this.renderFlags |= FlexRenderFlags.DirtyCheck - this.doCheck() - } + untracked(() => { + if (this.renderFlags & FlexRenderFlags.ViewFirstRender) { + } else { + if (previousContent !== content) { + this.renderFlags |= FlexRenderFlags.ContentChanged + } + if (previousProps !== props) { + this.renderFlags |= FlexRenderFlags.PropsReferenceChanged + } + } + + this.update() + }) + + previousContent = content + previousProps = props + }) + + inject(DestroyRef).onDestroy(() => { + if (this.#currentEffectRef) { + this.#currentEffectRef.destroy() + this.#currentEffectRef = null + this.renderView?.unmount() + this.renderView = null + } + }) } private doCheck() { @@ -175,35 +178,15 @@ export class FlexRender< this.update() } - #tableChangeEffect: EffectRef | null = null - - private bindTableDirtyCheck() { - this.#tableChangeEffect?.destroy() - this.#tableChangeEffect = null - let firstCheck = !!(this.renderFlags & FlexRenderFlags.ViewFirstRender) - if (this.table && this.notifier() === 'tableChange') { - const table = this.table - this.#tableChangeEffect = effect( - () => { - table.get() - if (firstCheck) { - firstCheck = false - return - } - this.renderFlags |= FlexRenderFlags.DirtyCheck - this.doCheck() - }, - { injector: this.#injector }, - ) - } - } - update() { if ( this.renderFlags & (FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender) ) { this.render() + if (FlexRenderFlags.ViewFirstRender & this.renderFlags) { + this.renderFlags &= ~FlexRenderFlags.ViewFirstRender + } return } if (this.renderFlags & FlexRenderFlags.PropsReferenceChanged) { @@ -259,9 +242,7 @@ export class FlexRender< return } this.renderFlags |= FlexRenderFlags.DirtySignal - // This will mark the view as changed, - // so we'll try to check for updates into ngDoCheck - this.#changeDetectorRef.markForCheck() + this.doCheck() }, { injector: this.viewContainerRef.injector }, ) @@ -313,11 +294,15 @@ export class FlexRender< template: Extract, ): FlexRenderTemplateView { const latestContext = () => this.props() - const view = this.viewContainerRef.createEmbeddedView(template.content, { - get $implicit() { - return latestContext() + const view = this.viewContainerRef.createEmbeddedView( + template.content, + { + get $implicit() { + return latestContext() + }, }, - }) + { injector: this.#getInjector() }, + ) return new FlexRenderTemplateView(template, view) } @@ -328,7 +313,7 @@ export class FlexRender< >, ): FlexRenderComponentView { const { injector } = flexRenderComponent.content - const componentInjector = this.#getComponentInjector(injector) + const componentInjector = this.#getInjector(injector) const view = this.#flexRenderComponentFactory.createComponent( flexRenderComponent.content, componentInjector, @@ -341,7 +326,7 @@ export class FlexRender< ): FlexRenderComponentView { const instance = flexRenderComponent(component.content, { inputs: this.props(), - injector: this.#getComponentInjector(this.injector()), + injector: this.#getInjector(this.injector()), }) const view = this.#flexRenderComponentFactory.createComponent( instance, @@ -350,15 +335,39 @@ export class FlexRender< return new FlexRenderComponentView(component, view) } - #getComponentInjector(parentInjector?: Injector) { + #getInjector(parentInjector?: Injector) { const getContext = () => this.props() const proxy = new Proxy(this.props(), { get: (_, key) => getContext()[key as keyof typeof _], }) + + const staticProviders = [...this.staticProviders()] + const context = getContext() + if ('cell' in context) { + staticProviders.push({ + provide: CellContextToken, + useValue: () => context.cell, + }) + staticProviders.push({ + provide: TableContextToken, + useValue: () => context.table, + }) + } + if ('header' in context) { + staticProviders.push({ + provide: HeaderContextToken, + useValue: () => context.header, + }) + staticProviders.push({ + provide: TableContextToken, + useValue: () => context.table, + }) + } + return Injector.create({ parent: parentInjector ?? this.injector(), providers: [ - ...this.staticProviders(), + ...staticProviders, { provide: FlexRenderComponentProps, useValue: proxy }, ], }) From 022aab112e56eeda81c5749850ec667967151675 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 21:52:22 +0100 Subject: [PATCH 16/30] refactor flexRender implementation to work with signals, add FlexRenderCell docs --- .../angular-table/src/context/flex-render.ts | 53 ++++--- packages/angular-table/src/flex-render.ts | 135 +++++++----------- .../angular-table/src/flex-render/flags.ts | 18 +-- .../flex-render/flex-render-component-ref.ts | 4 +- packages/angular-table/src/index.ts | 5 + 5 files changed, 97 insertions(+), 118 deletions(-) diff --git a/packages/angular-table/src/context/flex-render.ts b/packages/angular-table/src/context/flex-render.ts index af7408842f..60e2df87a3 100644 --- a/packages/angular-table/src/context/flex-render.ts +++ b/packages/angular-table/src/context/flex-render.ts @@ -6,22 +6,44 @@ import { RowData, TableFeatures, } from '@tanstack/table-core' -import { FlexRender } from '../flex-render' -import { CellContextToken } from './cell' -import { HeaderContextToken } from './header' -import { TableContextToken } from './table' +import { FlexRenderDirective } from '../flex-render' +/** + * Simplified directive wrapper of `*flexRender`. + * + * Use this utility component to render headers, cells, or footers with custom markup. + * + * Only one prop (`cell`, `header`, or `footer`) may be passed based on the used selector. + * + * @example + * ```html + *
+ * + * + * ``` + * + * This replaces calling `*flexRender` directly like this: + * ```html + * + * + * + * ``` + * + * @see {FlexRender} + */ @Directive({ selector: 'ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]', - hostDirectives: [{ directive: FlexRender }], + hostDirectives: [{ directive: FlexRenderDirective }], }) -export class CellFlexRender< +export class FlexRenderCell< TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData, > { - readonly #flexRender = inject(FlexRender) + readonly #flexRender = inject( + FlexRenderDirective, + ) readonly cell = input>(undefined, { alias: 'flexRenderCell', @@ -40,36 +62,21 @@ export class CellFlexRender< const cell = this.cell() const header = this.header() const footer = this.footer() - const { content, props, staticProviders } = this.#flexRender + const { content, props } = this.#flexRender if (cell) { content.set(cell.column.columnDef.cell) props.set(cell.getContext()) - staticProviders.set([ - { provide: TableContextToken, useValue: () => cell.table }, - { provide: CellContextToken, useValue: () => cell }, - ]) } if (header) { content.set(header.column.columnDef.header) props.set(header.getContext()) - staticProviders.set([ - { provide: TableContextToken, useValue: () => header.table }, - { provide: HeaderContextToken, useValue: () => header }, - ]) } if (footer) { content.set(footer.column.columnDef.footer) props.set(footer.getContext()) - staticProviders.set([ - { provide: TableContextToken, useValue: () => footer.table }, - { - provide: HeaderContextToken, - useValue: () => footer, - }, - ]) } }) } diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index 70a21dbbe7..b89af3b8a3 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -3,7 +3,7 @@ import { Directive, EffectRef, Injector, - Provider, + InputSignal, TemplateRef, Type, ViewContainerRef, @@ -31,14 +31,12 @@ import { import { CellContextToken } from './context/cell' import { HeaderContextToken } from './context/header' import { TableContextToken } from './context/table' -import type { InputSignal } from '@angular/core' import type { FlexRenderTypedContent } from './flex-render/view' import type { CellContext, CellData, HeaderContext, RowData, - Table, TableFeatures, } from '@tanstack/table-core' @@ -68,7 +66,7 @@ export type FlexRenderInputContent> = selector: 'ng-template[flexRender]', standalone: true, }) -export class FlexRender< +export class FlexRenderDirective< TFeatures extends TableFeatures, TRowData extends RowData, TValue extends CellData, @@ -83,9 +81,7 @@ export class FlexRender< readonly inputContent: InputSignal> = input( undefined as FlexRenderInputContent, - { - alias: 'flexRender', - }, + { alias: 'flexRender' }, ) readonly content = linkedSignal(() => this.inputContent()) @@ -94,22 +90,14 @@ export class FlexRender< }) readonly props = linkedSignal(() => this.inputProps()) - readonly #injector = inject(Injector) - readonly inputInjector = input(this.#injector, { + readonly inputInjector = input(inject(Injector), { alias: 'flexRenderInjector', }) readonly injector = linkedSignal(() => this.inputInjector()) - readonly inputStaticProviders = input>([], { - alias: 'flexRenderStaticProviders', - }) - readonly staticProviders = linkedSignal(() => this.inputStaticProviders()) - - readonly viewContainerRef = inject(ViewContainerRef) + readonly #viewContainerRef = inject(ViewContainerRef) + readonly #templateRef = inject(TemplateRef) - readonly templateRef = inject(TemplateRef) - - table: Table | null = null renderFlags = FlexRenderFlags.ViewFirstRender renderView: FlexRenderView | null = null @@ -129,6 +117,18 @@ export class FlexRender< }) constructor() { + const destroyRef = inject(DestroyRef) + destroyRef.onDestroy(() => { + if (this.#currentEffectRef) { + this.#currentEffectRef.destroy() + this.#currentEffectRef = null + } + if (this.renderView) { + this.renderView.unmount() + this.renderView = null + } + }) + let previousContent: FlexRenderInputContent let previousProps: TProps @@ -136,32 +136,24 @@ export class FlexRender< const props = this.props() const content = this.content() - untracked(() => { - if (this.renderFlags & FlexRenderFlags.ViewFirstRender) { - } else { - if (previousContent !== content) { - this.renderFlags |= FlexRenderFlags.ContentChanged - } - if (previousProps !== props) { - this.renderFlags |= FlexRenderFlags.PropsReferenceChanged - } + if (!(this.renderFlags & FlexRenderFlags.ViewFirstRender)) { + if (previousContent !== content) { + this.renderFlags |= FlexRenderFlags.ContentChanged } + if (previousProps !== props) { + this.renderFlags |= FlexRenderFlags.PropsReferenceChanged + } + } - this.update() - }) + untracked(() => this.update()) + + if (FlexRenderFlags.ViewFirstRender & this.renderFlags) { + this.renderFlags &= ~FlexRenderFlags.ViewFirstRender + } previousContent = content previousProps = props }) - - inject(DestroyRef).onDestroy(() => { - if (this.#currentEffectRef) { - this.#currentEffectRef.destroy() - this.#currentEffectRef = null - this.renderView?.unmount() - this.renderView = null - } - }) } private doCheck() { @@ -184,55 +176,47 @@ export class FlexRender< (FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender) ) { this.render() - if (FlexRenderFlags.ViewFirstRender & this.renderFlags) { - this.renderFlags &= ~FlexRenderFlags.ViewFirstRender - } return } + if (this.renderFlags & FlexRenderFlags.PropsReferenceChanged) { if (this.renderView) this.renderView.updateProps(this.props()) this.renderFlags &= ~FlexRenderFlags.PropsReferenceChanged } - if ( - this.renderFlags & - (FlexRenderFlags.DirtyCheck | FlexRenderFlags.DirtySignal) - ) { + + if (this.renderFlags & FlexRenderFlags.Dirty) { if (this.renderView) this.renderView.dirtyCheck() - this.renderFlags &= ~( - FlexRenderFlags.DirtyCheck | FlexRenderFlags.DirtySignal - ) + this.renderFlags &= ~FlexRenderFlags.Dirty } } #currentEffectRef: EffectRef | null = null render() { + // When the view is recreated from scratch (content change or first render), + // we have to destroy the current effect listener since it will be recreated + // skipping the first call (FlexRenderFlags.RenderEffectChecked) if (this.#shouldRecreateEntireView() && this.#currentEffectRef) { this.#currentEffectRef.destroy() this.#currentEffectRef = null this.renderFlags &= ~FlexRenderFlags.RenderEffectChecked } - this.viewContainerRef.clear() + this.#viewContainerRef.clear() if (this.renderView) { this.renderView.unmount() this.renderView = null } this.renderFlags = - FlexRenderFlags.Pristine | (this.renderFlags & FlexRenderFlags.ViewFirstRender) | (this.renderFlags & FlexRenderFlags.RenderEffectChecked) const resolvedContent = this.#getContentValue() - if (resolvedContent.kind === 'null') { - this.renderView = null - } else { - this.renderView = this.#renderViewByContent(resolvedContent) - } - + this.renderView = this.#renderViewByContent(resolvedContent) // If the content is a function `content(props)`, we initialize an effect - // in order to react to changes if the given definition use signals. + // to react to changes. If the current fn uses signals, we will set the DirtySignal flag + // to re-schedule the component updates if (!this.#currentEffectRef && typeof this.content === 'function') { this.#currentEffectRef = effect( () => { @@ -241,10 +225,10 @@ export class FlexRender< this.renderFlags |= FlexRenderFlags.RenderEffectChecked return } - this.renderFlags |= FlexRenderFlags.DirtySignal + this.renderFlags |= FlexRenderFlags.Dirty this.doCheck() }, - { injector: this.viewContainerRef.injector }, + { injector: this.#viewContainerRef.injector }, ) } } @@ -282,7 +266,7 @@ export class FlexRender< ? content : content?.(this.props()) } - const ref = this.viewContainerRef.createEmbeddedView(this.templateRef, { + const ref = this.#viewContainerRef.createEmbeddedView(this.#templateRef, { get $implicit() { return context() }, @@ -294,7 +278,7 @@ export class FlexRender< template: Extract, ): FlexRenderTemplateView { const latestContext = () => this.props() - const view = this.viewContainerRef.createEmbeddedView( + const view = this.#viewContainerRef.createEmbeddedView( template.content, { get $implicit() { @@ -341,26 +325,23 @@ export class FlexRender< get: (_, key) => getContext()[key as keyof typeof _], }) - const staticProviders = [...this.staticProviders()] - const context = getContext() - if ('cell' in context) { - staticProviders.push({ - provide: CellContextToken, - useValue: () => context.cell, - }) + const staticProviders = [] + if ('table' in proxy) { staticProviders.push({ provide: TableContextToken, - useValue: () => context.table, + useValue: () => proxy.table, }) } - if ('header' in context) { + if ('cell' in proxy) { staticProviders.push({ - provide: HeaderContextToken, - useValue: () => context.header, + provide: CellContextToken, + useValue: () => proxy.cell, }) + } + if ('header' in proxy) { staticProviders.push({ - provide: TableContextToken, - useValue: () => context.table, + provide: HeaderContextToken, + useValue: () => proxy.header, }) } @@ -373,9 +354,3 @@ export class FlexRender< }) } } - -/** - * @deprecated Use `FlexRender` import instead. - * @alias FlexRender - */ -export const FlexRenderDirective = FlexRender diff --git a/packages/angular-table/src/flex-render/flags.ts b/packages/angular-table/src/flex-render/flags.ts index 7458f71143..34f82c6537 100644 --- a/packages/angular-table/src/flex-render/flags.ts +++ b/packages/angular-table/src/flex-render/flags.ts @@ -8,15 +8,11 @@ export enum FlexRenderFlags { * This is the initial state and will transition after the first ngDoCheck. */ ViewFirstRender = 1 << 0, - /** - * Represents a state where the view is not dirty, meaning no changes require rendering updates. - */ - Pristine = 1 << 1, /** * Indicates the `content` property has been modified or the view requires a complete re-render. * When this flag is enabled, the view will be cleared and recreated from scratch. */ - ContentChanged = 1 << 2, + ContentChanged = 1 << 1, /** * Indicates that the `props` property reference has changed. * When this flag is enabled, the view context is updated based on the type of the content. @@ -24,17 +20,15 @@ export enum FlexRenderFlags { * For Component view, inputs will be updated and view will be marked as dirty. * For TemplateRef and primitive values, view will be marked as dirty */ - PropsReferenceChanged = 1 << 3, + PropsReferenceChanged = 1 << 2, /** * Indicates that the current rendered view needs to be checked for changes. + * This will be set to true when `content(props)` result has changed or during + * forced update */ - DirtyCheck = 1 << 4, - /** - * Indicates that a signal within the `content(props)` result has changed - */ - DirtySignal = 1 << 5, + Dirty = 1 << 3, /** * Indicates that the first render effect has been checked at least one time. */ - RenderEffectChecked = 1 << 6, + RenderEffectChecked = 1 << 4, } diff --git a/packages/angular-table/src/flex-render/flex-render-component-ref.ts b/packages/angular-table/src/flex-render/flex-render-component-ref.ts index 34c09b6031..0486766ae0 100644 --- a/packages/angular-table/src/flex-render/flex-render-component-ref.ts +++ b/packages/angular-table/src/flex-render/flex-render-component-ref.ts @@ -25,9 +25,7 @@ export class FlexRenderComponentFactory { ): FlexRenderComponentRef { const componentRef = this.#viewContainerRef.createComponent( flexRenderComponent.component, - { - injector: componentInjector, - }, + { injector: componentInjector }, ) const view = new FlexRenderComponentRef( componentRef, diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index 738363b515..cb126e3c02 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -1,3 +1,6 @@ +import { FlexRenderCell } from './context/flex-render' +import { FlexRenderDirective } from './flex-render' + export * from '@tanstack/table-core' export * from './angularReactivityFeature' @@ -13,3 +16,5 @@ export * from './context/header' export * from './context/table' export * from './context/createTableHook' export * from './context/flex-render' + +export const FlexRender = [FlexRenderDirective, FlexRenderCell] as const From 96d928618ec1b7c5f32a07ab3e0b145f8e70be2e Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 21:52:30 +0100 Subject: [PATCH 17/30] cleanup examples --- .../angular/basic/src/app/app.component.html | 36 ++++--------------- .../src/app/app.component.html | 1 - .../src/app/app.component.ts | 15 +++----- .../src/app/components/cell-components.ts | 9 ----- 4 files changed, 10 insertions(+), 51 deletions(-) diff --git a/examples/angular/basic/src/app/app.component.html b/examples/angular/basic/src/app/app.component.html index 8f105b6da5..5a7105c76b 100644 --- a/examples/angular/basic/src/app/app.component.html +++ b/examples/angular/basic/src/app/app.component.html @@ -5,16 +5,8 @@ @for (header of headerGroup.headers; track header.id) { @if (!header.isPlaceholder) { - } } @@ -25,16 +17,8 @@ @for (row of table.getRowModel().rows; track row.id) { @for (cell of row.getAllCells(); track cell.id) { - } @@ -44,16 +28,8 @@ @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { @for (footer of footerGroup.headers; track footer.id) { - } diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index ca4394c6c2..331ef7b47c 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -56,5 +56,4 @@
- diff --git a/examples/angular/composable-tables/src/app/app.component.ts b/examples/angular/composable-tables/src/app/app.component.ts index 3f0b42bdcf..47dfa1cd5d 100644 --- a/examples/angular/composable-tables/src/app/app.component.ts +++ b/examples/angular/composable-tables/src/app/app.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { - CellFlexRender, FlexRender, TanStackTable, TanStackTableCell, @@ -10,11 +9,10 @@ import { import { NgComponentOutlet } from '@angular/common' import { createAppColumnHelper, injectAppTable } from './table' import { makeData } from './makeData' -import type { Person, Product } from './makeData' +import type { Person } from './makeData' // Create column helpers with TFeatures already bound - only need TData! const personColumnHelper = createAppColumnHelper() -const productColumnHelper = createAppColumnHelper() @Component({ selector: 'app-root', @@ -24,7 +22,6 @@ const productColumnHelper = createAppColumnHelper() TanStackTableCell, NgComponentOutlet, TanStackTable, - CellFlexRender, ], templateUrl: './app.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -56,17 +53,17 @@ export class AppComponent { personColumnHelper.accessor('status', { header: 'Status', footer: ({ header }) => flexRenderComponent(header.FooterColumnId), - cell: ({ cell }) => cell.StatusCell, + cell: ({ cell }) => flexRenderComponent(cell.StatusCell), }), personColumnHelper.accessor('progress', { header: 'Progress', footer: ({ header }) => flexRenderComponent(header.FooterSum), - cell: ({ cell }) => cell.ProgressCell, + cell: ({ cell }) => flexRenderComponent(cell.ProgressCell), }), personColumnHelper.display({ id: 'actions', header: 'Actions', - cell: ({ cell }) => cell.RowActionsCell, + cell: ({ cell }) => flexRenderComponent(cell.RowActionsCell), }), ]) @@ -80,8 +77,4 @@ export class AppComponent { onRefresh = () => { this.data.set([...makeData(5000)]) } - - constructor() {} - - rerender() {} } diff --git a/examples/angular/composable-tables/src/app/components/cell-components.ts b/examples/angular/composable-tables/src/app/components/cell-components.ts index 3ebf7f1426..7dc2ddc91f 100644 --- a/examples/angular/composable-tables/src/app/components/cell-components.ts +++ b/examples/angular/composable-tables/src/app/components/cell-components.ts @@ -1,12 +1,3 @@ -// /** -// * Cell-level components that use useCellContext -// * -// * These components can be used via the pre-bound cellComponents -// * in AppCell children, e.g., -// */ -// import { useCellContext } from '../hooks/table' -// - import { Component, computed } from '@angular/core' import { injectFlexRenderContext } from '@tanstack/angular-table' import { CurrencyPipe } from '@angular/common' From 87128f803a447c8c1565afa30a660dbc028c8894 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 22:36:52 +0100 Subject: [PATCH 18/30] cleanup structure --- .../row-selection/src/app/app.component.html | 16 ++------- .../row-selection/src/app/app.component.ts | 29 ++++----------- packages/angular-table/package.json | 2 +- .../src/angularReactivityFeature.ts | 2 +- .../angular-table/src/flex-render/view.ts | 2 +- .../src/{flex-render.ts => flexRender.ts} | 12 +++---- .../src/{context => helpers}/cell.ts | 6 ++-- .../{context => helpers}/createTableHook.ts | 36 +++++++++++-------- .../flexRenderCell.ts} | 2 +- .../src/{context => helpers}/header.ts | 6 ++-- .../src/{context => helpers}/table.ts | 6 ++-- packages/angular-table/src/index.ts | 22 ++++++------ .../flex-render/flex-render.unit.test.ts | 26 +++++++------- 13 files changed, 71 insertions(+), 96 deletions(-) rename packages/angular-table/src/{flex-render.ts => flexRender.ts} (97%) rename packages/angular-table/src/{context => helpers}/cell.ts (89%) rename packages/angular-table/src/{context => helpers}/createTableHook.ts (94%) rename packages/angular-table/src/{context/flex-render.ts => helpers/flexRenderCell.ts} (97%) rename packages/angular-table/src/{context => helpers}/header.ts (88%) rename packages/angular-table/src/{context => helpers}/table.ts (88%) diff --git a/examples/angular/row-selection/src/app/app.component.html b/examples/angular/row-selection/src/app/app.component.html index 2d003e6809..b2929372c8 100644 --- a/examples/angular/row-selection/src/app/app.component.html +++ b/examples/angular/row-selection/src/app/app.component.html @@ -8,13 +8,7 @@ @for (header of headerGroup.headers; track header.id) { @for (cell of row.getAllCells(); track cell.id) { diff --git a/examples/angular/row-selection/src/app/app.component.ts b/examples/angular/row-selection/src/app/app.component.ts index 0b896ebd79..f2a28d09ba 100644 --- a/examples/angular/row-selection/src/app/app.component.ts +++ b/examples/angular/row-selection/src/app/app.component.ts @@ -6,8 +6,7 @@ import { viewChild, } from '@angular/core' import { - CellFlexRender, - FlexRenderDirective, + FlexRender, columnFilteringFeature, createFilteredRowModel, createPaginatedRowModel, @@ -44,7 +43,7 @@ const tableHelper = createTableHelper({ @Component({ selector: 'app-root', standalone: true, - imports: [FilterComponent, FlexRenderDirective, CellFlexRender, FormsModule], + imports: [FilterComponent, FlexRender, FormsModule], templateUrl: './app.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -53,20 +52,14 @@ export class AppComponent { readonly globalFilter = signal('') readonly data = signal(makeData(10_000)) - readonly myValue = signal(0) - readonly ageHeaderCell = viewChild.required>('ageHeaderCell') readonly columns: Array> = [ { id: 'select', - header: () => { - const data = flexRenderComponent(TableHeadSelectionComponent) - }, - cell: () => { - return flexRenderComponent(TableRowSelectionComponent) - }, + header: () => flexRenderComponent(TableHeadSelectionComponent), + cell: () => flexRenderComponent(TableRowSelectionComponent), }, { header: 'Name', @@ -76,14 +69,12 @@ export class AppComponent { accessorKey: 'firstName', cell: (info) => info.getValue(), footer: (props) => props.column.id, - header: (props) => `${this.myValue()} First name`, + header: (props) => `First name`, }, { accessorFn: (row) => row.lastName, id: 'lastName', - cell: (info) => { - return `lastname: ${info.getValue()} - is selected: ${info.row.getIsSelected()}` - }, + cell: (info) => info.getValue(), header: () => 'Last Name', footer: (props) => props.column.id, }, @@ -167,12 +158,4 @@ export class AppComponent { refreshData(): void { this.data.set(makeData(10_000)) } - - constructor() { - Reflect.set(window, 'updateValue', () => this.willUpdate()) - } - - willUpdate() { - this.myValue.update((x) => x + 1) - } } diff --git a/packages/angular-table/package.json b/packages/angular-table/package.json index 8d62694b8a..e4220ec4a5 100644 --- a/packages/angular-table/package.json +++ b/packages/angular-table/package.json @@ -40,7 +40,7 @@ "src" ], "scripts": { - "build": "ng-packagr -p ng-package.json -c tsconfig.build.json && rimraf ./build/lib/package.json", + "build": "ng-packagr -p ng-package.json -c tsconfig.build.json && rimraf ./dist/package.json", "build:types": "tsc --emitDeclarationOnly", "clean": "rimraf ./build && rimraf ./dist", "test:build": "publint --strict", diff --git a/packages/angular-table/src/angularReactivityFeature.ts b/packages/angular-table/src/angularReactivityFeature.ts index f43ff19ece..8e5453fd9f 100644 --- a/packages/angular-table/src/angularReactivityFeature.ts +++ b/packages/angular-table/src/angularReactivityFeature.ts @@ -66,7 +66,7 @@ const getUserSkipPropertyFn = ( return value ?? defaultPropertyFn } -export function constructAngularReactivityFeature< +function constructAngularReactivityFeature< TFeatures extends TableFeatures, TData extends RowData, >(): TableFeature> { diff --git a/packages/angular-table/src/flex-render/view.ts b/packages/angular-table/src/flex-render/view.ts index dc08fdce70..45310aec19 100644 --- a/packages/angular-table/src/flex-render/view.ts +++ b/packages/angular-table/src/flex-render/view.ts @@ -2,7 +2,7 @@ import { TemplateRef, Type } from '@angular/core' import { FlexRenderComponent } from './flex-render-component' import type { EmbeddedViewRef } from '@angular/core' import type { FlexRenderComponentRef } from './flex-render-component-ref' -import type { FlexRenderContent } from '../flex-render' +import type { FlexRenderContent } from '../flexRender' export type FlexRenderTypedContent = | { kind: 'null' } diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flexRender.ts similarity index 97% rename from packages/angular-table/src/flex-render.ts rename to packages/angular-table/src/flexRender.ts index b89af3b8a3..f3fe39e6fc 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flexRender.ts @@ -28,9 +28,9 @@ import { FlexRenderView, mapToFlexRenderTypedContent, } from './flex-render/view' -import { CellContextToken } from './context/cell' -import { HeaderContextToken } from './context/header' -import { TableContextToken } from './context/table' +import { TanStackTableCellToken } from './helpers/cell' +import { TanStackTableHeaderToken } from './helpers/header' +import { TanStackTableToken } from './helpers/table' import type { FlexRenderTypedContent } from './flex-render/view' import type { CellContext, @@ -328,19 +328,19 @@ export class FlexRenderDirective< const staticProviders = [] if ('table' in proxy) { staticProviders.push({ - provide: TableContextToken, + provide: TanStackTableToken, useValue: () => proxy.table, }) } if ('cell' in proxy) { staticProviders.push({ - provide: CellContextToken, + provide: TanStackTableCellToken, useValue: () => proxy.cell, }) } if ('header' in proxy) { staticProviders.push({ - provide: HeaderContextToken, + provide: TanStackTableHeaderToken, useValue: () => proxy.header, }) } diff --git a/packages/angular-table/src/context/cell.ts b/packages/angular-table/src/helpers/cell.ts similarity index 89% rename from packages/angular-table/src/context/cell.ts rename to packages/angular-table/src/helpers/cell.ts index f44e26f2f9..b54c98da67 100644 --- a/packages/angular-table/src/context/cell.ts +++ b/packages/angular-table/src/helpers/cell.ts @@ -10,7 +10,7 @@ export interface TanStackTableCellContext< cell: Signal> } -export const CellContextToken = new InjectionToken< +export const TanStackTableCellToken = new InjectionToken< TanStackTableCellContext['cell'] >('[TanStack Table] CellContext') @@ -19,7 +19,7 @@ export const CellContextToken = new InjectionToken< exportAs: 'cell', providers: [ { - provide: CellContextToken, + provide: TanStackTableCellToken, useFactory: () => inject(TanStackTableCell).cell, }, ], @@ -39,5 +39,5 @@ export function injectTableCellContext< TData extends RowData, TValue extends CellData, >(): TanStackTableCellContext['cell'] { - return inject(CellContextToken) + return inject(TanStackTableCellToken) } diff --git a/packages/angular-table/src/context/createTableHook.ts b/packages/angular-table/src/helpers/createTableHook.ts similarity index 94% rename from packages/angular-table/src/context/createTableHook.ts rename to packages/angular-table/src/helpers/createTableHook.ts index f7c888cd4c..55a2442b39 100644 --- a/packages/angular-table/src/context/createTableHook.ts +++ b/packages/angular-table/src/helpers/createTableHook.ts @@ -1,8 +1,10 @@ import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' import { injectTable } from '../injectTable' +import { injectFlexRenderContext } from '../flexRender' import { injectTableHeaderContext as _injectTableHeaderContext } from './header' -import { injectTableContext as _injetTableContext } from './table' +import { injectTableContext as _injectTableContext } from './table' import { injectTableCellContext as _injectTableCellContext } from './cell' +import type { FlexRenderContent } from '../flexRender' import type { AngularTable } from '../injectTable' import type { AccessorFn, @@ -18,6 +20,7 @@ import type { DisplayColumnDef, GroupColumnDef, Header, + HeaderContext, IdentifiedColumnDef, Row, RowData, @@ -28,7 +31,6 @@ import type { TableState, } from '@tanstack/table-core' import type { Type } from '@angular/core' -import type { FlexRenderContent } from '../flex-render' type RenderableComponent = | Type @@ -287,18 +289,6 @@ export type CreateTableContextOptions< headerComponents?: THeaderComponents } -/** - * Props for AppCell component - */ -export interface AppCellPropsWithoutSelector< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData, - TCellComponents extends Record, -> { - cell: Cell -} - export function createTableHook< TFeatures extends TableFeatures, const TTableComponents extends Record, @@ -316,7 +306,7 @@ export function createTableHook< THeaderComponents >) { function injectTableContext() { - return _injetTableContext() + return _injectTableContext() } function injectTableHeaderContext() { @@ -327,6 +317,20 @@ export function createTableHook< return _injectTableCellContext() } + function injectFlexRenderHeaderContext< + TData extends RowData, + TValue extends CellData, + >() { + return injectFlexRenderContext>() + } + + function injectFlexRenderCellContext< + TData extends RowData, + TValue extends CellData, + >() { + return injectFlexRenderContext>() + } + function injectAppTable( tableOptions: () => Omit< TableOptions, @@ -384,6 +388,8 @@ export function createTableHook< injectTableContext, injectTableHeaderContext, injectTableCellContext, + injectFlexRenderHeaderContext, + injectFlexRenderCellContext, injectAppTable, } } diff --git a/packages/angular-table/src/context/flex-render.ts b/packages/angular-table/src/helpers/flexRenderCell.ts similarity index 97% rename from packages/angular-table/src/context/flex-render.ts rename to packages/angular-table/src/helpers/flexRenderCell.ts index 60e2df87a3..be575621ae 100644 --- a/packages/angular-table/src/context/flex-render.ts +++ b/packages/angular-table/src/helpers/flexRenderCell.ts @@ -6,7 +6,7 @@ import { RowData, TableFeatures, } from '@tanstack/table-core' -import { FlexRenderDirective } from '../flex-render' +import { FlexRenderDirective } from '../flexRender' /** * Simplified directive wrapper of `*flexRender`. diff --git a/packages/angular-table/src/context/header.ts b/packages/angular-table/src/helpers/header.ts similarity index 88% rename from packages/angular-table/src/context/header.ts rename to packages/angular-table/src/helpers/header.ts index b36a7c9529..d80e302ad9 100644 --- a/packages/angular-table/src/context/header.ts +++ b/packages/angular-table/src/helpers/header.ts @@ -2,7 +2,7 @@ import { Directive, InjectionToken, inject, input } from '@angular/core' import { CellData, Header, RowData, TableFeatures } from '@tanstack/table-core' import type { Signal } from '@angular/core' -export const HeaderContextToken = new InjectionToken< +export const TanStackTableHeaderToken = new InjectionToken< TanStackTableHeaderContext['header'] >('[TanStack Table] HeaderContext') @@ -19,7 +19,7 @@ export interface TanStackTableHeaderContext< exportAs: 'header', providers: [ { - provide: HeaderContextToken, + provide: TanStackTableHeaderToken, useFactory: () => inject(TanStackTableHeader).header, }, ], @@ -39,5 +39,5 @@ export function injectTableHeaderContext< TData extends RowData, TValue extends CellData, >(): TanStackTableHeaderContext['header'] { - return inject(HeaderContextToken) + return inject(TanStackTableHeaderToken) } diff --git a/packages/angular-table/src/context/table.ts b/packages/angular-table/src/helpers/table.ts similarity index 88% rename from packages/angular-table/src/context/table.ts rename to packages/angular-table/src/helpers/table.ts index 1a74ff231c..9262b6ad4d 100644 --- a/packages/angular-table/src/context/table.ts +++ b/packages/angular-table/src/helpers/table.ts @@ -2,7 +2,7 @@ import { Directive, InjectionToken, inject, input } from '@angular/core' import { RowData, Table, TableFeatures } from '@tanstack/table-core' import type { Signal } from '@angular/core' -export const TableContextToken = new InjectionToken< +export const TanStackTableToken = new InjectionToken< TanStackTableContext['table'] >('[TanStack Table] HeaderContext') @@ -18,7 +18,7 @@ export interface TanStackTableContext< exportAs: 'table', providers: [ { - provide: TableContextToken, + provide: TanStackTableToken, useFactory: () => inject(TanStackTable).table, }, ], @@ -36,5 +36,5 @@ export function injectTableContext< TFeatures extends TableFeatures, TData extends RowData, >(): TanStackTableContext['table'] { - return inject(TableContextToken) + return inject(TanStackTableToken) } diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index cb126e3c02..7045d58ba8 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -1,20 +1,20 @@ -import { FlexRenderCell } from './context/flex-render' -import { FlexRenderDirective } from './flex-render' +import { FlexRenderCell } from './helpers/flexRenderCell' +import { FlexRenderDirective } from './flexRender' export * from '@tanstack/table-core' -export * from './angularReactivityFeature' +// export * from './angularReactivityFeature' export * from './createTableHelper' -export * from './flex-render' +export * from './flexRender' export * from './injectTable' -export * from './lazySignalInitializer' -export * from './reactivityUtils' +// export * from './lazySignalInitializer' +// export * from './reactivityUtils' export * from './flex-render/flex-render-component' -export * from './context/cell' -export * from './context/header' -export * from './context/table' -export * from './context/createTableHook' -export * from './context/flex-render' +export * from './helpers/cell' +export * from './helpers/header' +export * from './helpers/table' +export * from './helpers/createTableHook' +export * from './helpers/flexRenderCell' export const FlexRender = [FlexRenderDirective, FlexRenderCell] as const diff --git a/packages/angular-table/tests/flex-render/flex-render.unit.test.ts b/packages/angular-table/tests/flex-render/flex-render.unit.test.ts index c366867354..96ce5241aa 100644 --- a/packages/angular-table/tests/flex-render/flex-render.unit.test.ts +++ b/packages/angular-table/tests/flex-render/flex-render.unit.test.ts @@ -5,10 +5,10 @@ import { describe, expect, test } from 'vitest' import { FlexRender, FlexRenderDirective, + flexRenderComponent, injectFlexRenderContext, -} from '../../src/flex-render' +} from '../../src' import { setFixtureSignalInput, setFixtureSignalInputs } from '../test-utils' -import { flexRenderComponent } from '../../src/flex-render/flex-render-component' import type { TemplateRef } from '@angular/core' import type { ComponentFixture } from '@angular/core/testing' @@ -167,28 +167,26 @@ describe('FlexRenderDirective', () => { @Component({ selector: 'app-test-render', template: ` - + `, standalone: true, - imports: [FlexRenderDirective], + imports: [FlexRender], }) class TestRenderComponent { - readonly content = input.required() + readonly content = input.required() readonly context = input.required>() } -type FlexRenderDirectiveAllowedContent = ReturnType< - FlexRender>['content'] +type FlexRenderAllowedContent = ReturnType< + FlexRenderDirective< + any, + any, + NonNullable, + NonNullable + >['content'] > function expectPrimitiveValueIs( From 6fb4bac4239d228f339efaa6c84b9addd60aa35e Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 22:44:01 +0100 Subject: [PATCH 19/30] table state selector fix for angular --- examples/angular/column-ordering/src/app/app.component.ts | 2 +- .../angular/column-pinning-sticky/src/app/app.component.ts | 2 +- examples/angular/column-pinning/src/app/app.component.ts | 2 +- examples/angular/column-visibility/src/app/app.component.ts | 2 +- examples/angular/editable/src/app/app.component.html | 2 +- packages/angular-table/src/injectTable.ts | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/angular/column-ordering/src/app/app.component.ts b/examples/angular/column-ordering/src/app/app.component.ts index 912bc682ee..66220503fa 100644 --- a/examples/angular/column-ordering/src/app/app.component.ts +++ b/examples/angular/column-ordering/src/app/app.component.ts @@ -112,7 +112,7 @@ export class AppComponent { })) readonly stringifiedColumnOrdering = computed(() => { - return JSON.stringify(this.table.store.state.columnOrder) + return JSON.stringify(this.table.state().columnOrder) }) randomizeColumns() { diff --git a/examples/angular/column-pinning-sticky/src/app/app.component.ts b/examples/angular/column-pinning-sticky/src/app/app.component.ts index eb5a4a0610..e92c532dec 100644 --- a/examples/angular/column-pinning-sticky/src/app/app.component.ts +++ b/examples/angular/column-pinning-sticky/src/app/app.component.ts @@ -111,7 +111,7 @@ export class AppComponent { })) stringifiedColumnPinning = computed(() => { - return JSON.stringify(this.table.store.state.columnPinning) + return JSON.stringify(this.table.state().columnPinning) }) readonly getCommonPinningStyles = ( diff --git a/examples/angular/column-pinning/src/app/app.component.ts b/examples/angular/column-pinning/src/app/app.component.ts index dca59189ee..c01ed66eeb 100644 --- a/examples/angular/column-pinning/src/app/app.component.ts +++ b/examples/angular/column-pinning/src/app/app.component.ts @@ -135,7 +135,7 @@ export class AppComponent { })) stringifiedColumnPinning = computed(() => { - return JSON.stringify(this.table.store.state.columnPinning) + return JSON.stringify(this.table.state().columnPinning) }) randomizeColumns() { diff --git a/examples/angular/column-visibility/src/app/app.component.ts b/examples/angular/column-visibility/src/app/app.component.ts index 74e2168642..999ac11538 100644 --- a/examples/angular/column-visibility/src/app/app.component.ts +++ b/examples/angular/column-visibility/src/app/app.component.ts @@ -135,7 +135,7 @@ export class AppComponent implements OnInit { })) stringifiedColumnVisibility = computed(() => { - return JSON.stringify(this.table.store.state.columnVisibility) + return JSON.stringify(this.table.state().columnVisibility) }) ngOnInit() { diff --git a/examples/angular/editable/src/app/app.component.html b/examples/angular/editable/src/app/app.component.html index 37420e6395..d8cff86033 100644 --- a/examples/angular/editable/src/app/app.component.html +++ b/examples/angular/editable/src/app/app.component.html @@ -94,7 +94,7 @@
Page
- {{ table.store.state.pagination.pageIndex + 1 }} of + {{ table.state().pagination.pageIndex + 1 }} of {{ table.getPageCount() }}
diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index e1897e70f4..d99eb1a646 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -40,11 +40,11 @@ export type AngularTable< export function injectTable< TFeatures extends TableFeatures, TData extends RowData, - TSelected = {}, + TSelected = TableState, >( options: () => TableOptions, - selector: (state: TableState) => TSelected = () => - ({}) as TSelected, + selector: (state: TableState) => TSelected = (state) => + state as TSelected, ): AngularTable { assertInInjectionContext(injectTable) const injector = inject(Injector) From 959157290aae79be207beaddab464fea62d48bfa Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 23:01:10 +0100 Subject: [PATCH 20/30] basic app table example --- .../.devcontainer/devcontainer.json | 4 + .../angular/basic-app-table/.editorconfig | 16 +++ examples/angular/basic-app-table/.gitignore | 42 +++++++ examples/angular/basic-app-table/README.md | 27 ++++ examples/angular/basic-app-table/angular.json | 107 ++++++++++++++++ examples/angular/basic-app-table/package.json | 34 +++++ .../src/app/app.component.html | 42 +++++++ .../basic-app-table/src/app/app.component.ts | 119 ++++++++++++++++++ .../basic-app-table/src/app/app.config.ts | 5 + .../basic-app-table/src/assets/.gitkeep | 0 .../angular/basic-app-table/src/favicon.ico | Bin 0 -> 15086 bytes .../angular/basic-app-table/src/index.html | 14 +++ examples/angular/basic-app-table/src/main.ts | 5 + .../angular/basic-app-table/src/styles.scss | 32 +++++ .../angular/basic-app-table/tsconfig.app.json | 10 ++ .../angular/basic-app-table/tsconfig.json | 31 +++++ .../basic-app-table/tsconfig.spec.json | 9 ++ .../angular/basic/src/app/app.component.ts | 14 ++- pnpm-lock.yaml | 55 ++++++++ 19 files changed, 563 insertions(+), 3 deletions(-) create mode 100644 examples/angular/basic-app-table/.devcontainer/devcontainer.json create mode 100644 examples/angular/basic-app-table/.editorconfig create mode 100644 examples/angular/basic-app-table/.gitignore create mode 100644 examples/angular/basic-app-table/README.md create mode 100644 examples/angular/basic-app-table/angular.json create mode 100644 examples/angular/basic-app-table/package.json create mode 100644 examples/angular/basic-app-table/src/app/app.component.html create mode 100644 examples/angular/basic-app-table/src/app/app.component.ts create mode 100644 examples/angular/basic-app-table/src/app/app.config.ts create mode 100644 examples/angular/basic-app-table/src/assets/.gitkeep create mode 100644 examples/angular/basic-app-table/src/favicon.ico create mode 100644 examples/angular/basic-app-table/src/index.html create mode 100644 examples/angular/basic-app-table/src/main.ts create mode 100644 examples/angular/basic-app-table/src/styles.scss create mode 100644 examples/angular/basic-app-table/tsconfig.app.json create mode 100644 examples/angular/basic-app-table/tsconfig.json create mode 100644 examples/angular/basic-app-table/tsconfig.spec.json diff --git a/examples/angular/basic-app-table/.devcontainer/devcontainer.json b/examples/angular/basic-app-table/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..36f47d8762 --- /dev/null +++ b/examples/angular/basic-app-table/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/basic-app-table/.editorconfig b/examples/angular/basic-app-table/.editorconfig new file mode 100644 index 0000000000..59d9a3a3e7 --- /dev/null +++ b/examples/angular/basic-app-table/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/angular/basic-app-table/.gitignore b/examples/angular/basic-app-table/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/examples/angular/basic-app-table/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/basic-app-table/README.md b/examples/angular/basic-app-table/README.md new file mode 100644 index 0000000000..5da97a87d1 --- /dev/null +++ b/examples/angular/basic-app-table/README.md @@ -0,0 +1,27 @@ +# Basic + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/basic-app-table/angular.json b/examples/angular/basic-app-table/angular.json new file mode 100644 index 0000000000..b121ae5757 --- /dev/null +++ b/examples/angular/basic-app-table/angular.json @@ -0,0 +1,107 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "basic-app-table": { + "cli": { + "cache": { + "enabled": false + } + }, + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true, + "style": "scss" + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/basic-app-table", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "basic-app-table:build:production" + }, + "development": { + "buildTarget": "basic-app-table:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "basic-app-table:build" + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/examples/angular/basic-app-table/package.json b/examples/angular/basic-app-table/package.json new file mode 100644 index 0000000000..7ab7b072af --- /dev/null +++ b/examples/angular/basic-app-table/package.json @@ -0,0 +1,34 @@ +{ + "name": "tanstack-table-example-angular-basic", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "lint": "eslint ./src" + }, + "private": true, + "dependencies": { + "@angular/common": "^21.0.6", + "@angular/compiler": "^21.0.6", + "@angular/core": "^21.0.6", + "@angular/forms": "^21.0.6", + "@angular/platform-browser": "^21.0.6", + "@angular/platform-browser-dynamic": "^21.0.6", + "@angular/router": "^21.0.6", + "@tanstack/angular-table": "^9.0.0-alpha.10", + "rxjs": "~7.8.2", + "zone.js": "~0.16.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.6", + "@types/jasmine": "~5.1.13", + "jasmine-core": "~5.13.0", + "tslib": "^2.8.1", + "typescript": "5.9.3" + } +} diff --git a/examples/angular/basic-app-table/src/app/app.component.html b/examples/angular/basic-app-table/src/app/app.component.html new file mode 100644 index 0000000000..5a7105c76b --- /dev/null +++ b/examples/angular/basic-app-table/src/app/app.component.html @@ -0,0 +1,42 @@ +
+
- -
+ + {{ header }}
- -
+ @for (cell of row.getAllCells(); track cell.id) { +
+ + {{ cell}}
- - {{ footer }} - + + @if (!footer.isPlaceholder) { + + {{ footer }} + + }
- {{ cell}} + {{ cell }} {{cell}}{{header}}{{footer}}{{cell}}{{header}}{{footer}}
- -
-
+
+
- -
-
+
+
- - {{ footer }} - + + {{ footer }}
@if (!header.isPlaceholder) { - + {{ headerCell }} @@ -38,13 +32,7 @@
- + {{ renderCell }}
+ + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + @if (!header.isPlaceholder) { + + } + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + + + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (footer of footerGroup.headers; track footer.id) { + + } + + } + +
+
+
+
+
+ {{ footer }} +
+ +
+ +
diff --git a/examples/angular/basic-app-table/src/app/app.component.ts b/examples/angular/basic-app-table/src/app/app.component.ts new file mode 100644 index 0000000000..b1af1512db --- /dev/null +++ b/examples/angular/basic-app-table/src/app/app.component.ts @@ -0,0 +1,119 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { FlexRender, createTableHook } from '@tanstack/angular-table' + +// This example uses the new `createTableHook` method to create a re-usable table hook factory instead of independently +// using the standalone `useTable` hook and `createColumnHelper` method. You can choose to use either way. + +// 1. Define what the shape of your data will be for each row +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +// 2. Create some dummy data with a stable reference (this could be an API response stored in useState or similar) +const defaultData: Array = [ + { + firstName: 'tanner', + lastName: 'linsley', + age: 24, + visits: 100, + status: 'In Relationship', + progress: 50, + }, + { + firstName: 'tandy', + lastName: 'miller', + age: 40, + visits: 40, + status: 'Single', + progress: 80, + }, + { + firstName: 'joe', + lastName: 'dirte', + age: 45, + visits: 20, + status: 'Complicated', + progress: 10, + }, + { + firstName: 'kevin', + lastName: 'vandy', + age: 28, + visits: 100, + status: 'Single', + progress: 70, + }, +] + +// 3. New in V9! Tell the table which features and row models we want to use. +// In this case, this will be a basic table with no additional features +const { injectAppTable, createAppColumnHelper } = createTableHook({ + _features: {}, + _rowModels: {}, // client-side row models. `Core` row model is now included by default, but you can still override it here + debugTable: true, +}) + +// 4. Create a helper object to help define our columns +const columnHelper = createAppColumnHelper() + +// 5. Define the columns for your table with a stable reference (in this case, defined statically outside of a react component) +const columns = columnHelper.columns([ + // accessorKey method (most common for simple use-cases) + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (info) => info.column.id, + }), + // accessorFn used (alternative) along with a custom id + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => `${info.getValue()}`, + header: () => `Last Name`, + footer: (info) => info.column.id, + }), + // accessorFn used to transform the data + columnHelper.accessor((row) => Number(row.age), { + id: 'age', + header: () => 'Age', + cell: (info) => info.renderValue(), + footer: (info) => info.column.id, + }), + columnHelper.accessor('visits', { + header: () => `Visits`, + footer: (info) => info.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (info) => info.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (info) => info.column.id, + }), +]) + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + readonly data = signal>(defaultData) + + // 6. Create the table instance with the required columns and data. + // Features and row models are already defined in the createTableHook call above + readonly table = injectAppTable(() => ({ + columns, + data: this.data(), + // add additional table options here or in the createTableHook call above + })) + + rerender() { + this.data.set([...defaultData.sort(() => -1)]) + } +} diff --git a/examples/angular/basic-app-table/src/app/app.config.ts b/examples/angular/basic-app-table/src/app/app.config.ts new file mode 100644 index 0000000000..f997e614ac --- /dev/null +++ b/examples/angular/basic-app-table/src/app/app.config.ts @@ -0,0 +1,5 @@ +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [], +} diff --git a/examples/angular/basic-app-table/src/assets/.gitkeep b/examples/angular/basic-app-table/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/angular/basic-app-table/src/favicon.ico b/examples/angular/basic-app-table/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/examples/angular/basic-app-table/src/index.html b/examples/angular/basic-app-table/src/index.html new file mode 100644 index 0000000000..e4955ab6cb --- /dev/null +++ b/examples/angular/basic-app-table/src/index.html @@ -0,0 +1,14 @@ + + + + + Basic App Table + + + + + + + + + diff --git a/examples/angular/basic-app-table/src/main.ts b/examples/angular/basic-app-table/src/main.ts new file mode 100644 index 0000000000..c3d8f9af99 --- /dev/null +++ b/examples/angular/basic-app-table/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/basic-app-table/src/styles.scss b/examples/angular/basic-app-table/src/styles.scss new file mode 100644 index 0000000000..cda3113f7d --- /dev/null +++ b/examples/angular/basic-app-table/src/styles.scss @@ -0,0 +1,32 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +.pagination-actions { + margin: 10px; + display: flex; + gap: 10px; +} diff --git a/examples/angular/basic-app-table/tsconfig.app.json b/examples/angular/basic-app-table/tsconfig.app.json new file mode 100644 index 0000000000..84f1f992d2 --- /dev/null +++ b/examples/angular/basic-app-table/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/basic-app-table/tsconfig.json b/examples/angular/basic-app-table/tsconfig.json new file mode 100644 index 0000000000..b58d3efc71 --- /dev/null +++ b/examples/angular/basic-app-table/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "src", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/basic-app-table/tsconfig.spec.json b/examples/angular/basic-app-table/tsconfig.spec.json new file mode 100644 index 0000000000..47e3dd7551 --- /dev/null +++ b/examples/angular/basic-app-table/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/examples/angular/basic/src/app/app.component.ts b/examples/angular/basic/src/app/app.component.ts index 0150b025ec..eddc314cfb 100644 --- a/examples/angular/basic/src/app/app.component.ts +++ b/examples/angular/basic/src/app/app.component.ts @@ -2,6 +2,9 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table' import type { ColumnDef } from '@tanstack/angular-table' +// This example uses the classic standalone `useTable` hook to create a table without the new `createTableHelper` util. + +// 1. Define what the shape of your data will be for each row type Person = { firstName: string lastName: string @@ -11,6 +14,7 @@ type Person = { progress: number } +// 2. Create some dummy data const defaultData: Array = [ { firstName: 'tanner', @@ -38,8 +42,12 @@ const defaultData: Array = [ }, ] -const _features = tableFeatures({}) +// 3. New in V9! Tell the table which features and row models we want to use. +// In this case, this will be a basic table with no additional features +const _features = tableFeatures({}) // util method to create sharable TFeatures object/type +// 4. Define the columns for your table. This uses the new `ColumnDef` type to define columns. +// Alternatively, check out the createTableHelper/createColumnHelper util for an even more type-safe way to define columns. const defaultColumns: Array> = [ { accessorKey: 'firstName', @@ -84,13 +92,13 @@ const defaultColumns: Array> = [ export class AppComponent { readonly data = signal>(defaultData) + // 5. Create the table instance with required _features, columns, and data table = injectTable(() => ({ _features, // new required option in V9. Tell the table which features you are importing and using (better tree-shaking) _rowModels: {}, // `Core` row model is now included by default, but you can still override it here data: this.data(), columns: defaultColumns, - debugTable: true, - // other options here + // ...other options here })) rerender() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 464565dffb..c008c3a462 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,61 @@ importers: specifier: 5.9.3 version: 5.9.3 + examples/angular/basic-app-table: + dependencies: + '@angular/common': + specifier: ^21.0.6 + version: 21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.0.6 + version: 21.0.6 + '@angular/core': + specifier: ^21.0.6 + version: 21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/forms': + specifier: ^21.0.6 + version: 21.0.6(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + '@angular/platform-browser': + specifier: ^21.0.6 + version: 21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)) + '@angular/platform-browser-dynamic': + specifier: ^21.0.6 + version: 21.0.6(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@21.0.6)(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))) + '@angular/router': + specifier: ^21.0.6 + version: 21.0.6(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@21.0.6(@angular/animations@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@21.0.6(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.0.6(@angular/compiler@21.0.6)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + '@tanstack/angular-table': + specifier: ^9.0.0-alpha.10 + version: link:../../../packages/angular-table + rxjs: + specifier: ~7.8.2 + version: 7.8.2 + zone.js: + specifier: ~0.16.0 + version: 0.16.0 + devDependencies: + '@angular/build': + specifier: ^21.0.4 + version: 21.0.4(kc35yzw5n5t7efydd2g6bmpsfy) + '@angular/cli': + specifier: ^21.0.4 + version: 21.0.4(@types/node@25.0.3)(chokidar@4.0.3) + '@angular/compiler-cli': + specifier: ^21.0.6 + version: 21.0.6(@angular/compiler@21.0.6)(typescript@5.9.3) + '@types/jasmine': + specifier: ~5.1.13 + version: 5.1.13 + jasmine-core: + specifier: ~5.13.0 + version: 5.13.0 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + examples/angular/column-ordering: dependencies: '@angular/common': From bc95e00ccdb11c054700eed42e1ad2c0b4188b96 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 11 Jan 2026 23:04:43 +0100 Subject: [PATCH 21/30] code review fixes --- .../src/app/components/cell-components.ts | 11 ++--------- .../src/app/components/header-components.ts | 4 ++-- examples/angular/composable-tables/src/index.html | 2 +- packages/angular-table/src/flexRender.ts | 5 ++++- packages/angular-table/src/helpers/table.ts | 2 +- packages/angular-table/src/lazySignalInitializer.ts | 1 - 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/examples/angular/composable-tables/src/app/components/cell-components.ts b/examples/angular/composable-tables/src/app/components/cell-components.ts index 7dc2ddc91f..a6c20ad3a6 100644 --- a/examples/angular/composable-tables/src/app/components/cell-components.ts +++ b/examples/angular/composable-tables/src/app/components/cell-components.ts @@ -57,7 +57,7 @@ export class ProgressCell { } @Component({ - selector: 'table-progress-cell', + selector: 'table-row-actions', template: `
@@ -90,18 +90,11 @@ export class RowActionsCell { @Component({ selector: 'table-price-cell', - template: ` {{ price() | currency }} `, + template: ` {{ cell().getValue() | currency }} `, imports: [CurrencyPipe], }) export class PriceCell { readonly cell = injectTableCellContext() - - readonly price = computed(() => - this.cell().getValue().toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }), - ) } @Component({ diff --git a/examples/angular/composable-tables/src/app/components/header-components.ts b/examples/angular/composable-tables/src/app/components/header-components.ts index 168c204edf..2d23c41b53 100644 --- a/examples/angular/composable-tables/src/app/components/header-components.ts +++ b/examples/angular/composable-tables/src/app/components/header-components.ts @@ -26,7 +26,7 @@ import { injectTableHeaderContext } from '../table' @Component({ selector: 'span', host: { - 'tantack-footer-column-id': '', + 'tanstack-footer-column-id': '', class: 'footer-column-id', }, template: `{{ header().column.id }}`, @@ -38,7 +38,7 @@ export class FooterColumnId { @Component({ selector: 'span', host: { - 'tantack-footer-sum': '', + 'tanstack-footer-sum': '', class: 'footer-sum', }, template: `{{ sum() > 0 ? sum().toLocaleString() : '—' }}`, diff --git a/examples/angular/composable-tables/src/index.html b/examples/angular/composable-tables/src/index.html index a4bb987648..9a37746785 100644 --- a/examples/angular/composable-tables/src/index.html +++ b/examples/angular/composable-tables/src/index.html @@ -2,7 +2,7 @@ - Basic + Composable tables diff --git a/packages/angular-table/src/flexRender.ts b/packages/angular-table/src/flexRender.ts index f3fe39e6fc..197857e5ee 100644 --- a/packages/angular-table/src/flexRender.ts +++ b/packages/angular-table/src/flexRender.ts @@ -217,7 +217,10 @@ export class FlexRenderDirective< // If the content is a function `content(props)`, we initialize an effect // to react to changes. If the current fn uses signals, we will set the DirtySignal flag // to re-schedule the component updates - if (!this.#currentEffectRef && typeof this.content === 'function') { + if ( + !this.#currentEffectRef && + typeof untracked(this.content) === 'function' + ) { this.#currentEffectRef = effect( () => { this.#latestContent() diff --git a/packages/angular-table/src/helpers/table.ts b/packages/angular-table/src/helpers/table.ts index 9262b6ad4d..4baec5264f 100644 --- a/packages/angular-table/src/helpers/table.ts +++ b/packages/angular-table/src/helpers/table.ts @@ -4,7 +4,7 @@ import type { Signal } from '@angular/core' export const TanStackTableToken = new InjectionToken< TanStackTableContext['table'] ->('[TanStack Table] HeaderContext') +>('[TanStack Table] Table Context') export interface TanStackTableContext< TFeatures extends TableFeatures, diff --git a/packages/angular-table/src/lazySignalInitializer.ts b/packages/angular-table/src/lazySignalInitializer.ts index 1d092d68d7..92f8dcc901 100644 --- a/packages/angular-table/src/lazySignalInitializer.ts +++ b/packages/angular-table/src/lazySignalInitializer.ts @@ -6,7 +6,6 @@ import { untracked } from '@angular/core' */ export function lazyInit(initializer: () => T): T { let object: T | null = null - const addedPropsDuringInitialization = {} const initializeObject = () => { if (!object) { From 60028270e676905bd0740431f6665ad2806def24 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 21:49:12 +0100 Subject: [PATCH 22/30] flex render run content(props) in injection context --- packages/angular-table/src/flexRender.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/angular-table/src/flexRender.ts b/packages/angular-table/src/flexRender.ts index 197857e5ee..81b03c1d9b 100644 --- a/packages/angular-table/src/flexRender.ts +++ b/packages/angular-table/src/flexRender.ts @@ -267,7 +267,7 @@ export class FlexRenderDirective< const content = this.content() return typeof content === 'string' || typeof content === 'number' ? content - : content?.(this.props()) + : runInInjectionContext(this.injector(), () => content?.(this.props())) } const ref = this.#viewContainerRef.createEmbeddedView(this.#templateRef, { get $implicit() { @@ -313,11 +313,11 @@ export class FlexRenderDirective< ): FlexRenderComponentView { const instance = flexRenderComponent(component.content, { inputs: this.props(), - injector: this.#getInjector(this.injector()), }) + const injector = this.#getInjector(instance.injector) const view = this.#flexRenderComponentFactory.createComponent( instance, - this.injector(), + injector, ) return new FlexRenderComponentView(component, view) } From 173db1d5afb282385e270d7fa45b6a8bb562baee Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 21:50:46 +0100 Subject: [PATCH 23/30] createTableHook add typed helpers --- .../src/helpers/createTableHook.ts | 35 ++++++++++++++++--- packages/angular-table/src/helpers/table.ts | 20 +++++------ packages/angular-table/src/injectTable.ts | 2 +- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/angular-table/src/helpers/createTableHook.ts b/packages/angular-table/src/helpers/createTableHook.ts index 55a2442b39..bff4da52e6 100644 --- a/packages/angular-table/src/helpers/createTableHook.ts +++ b/packages/angular-table/src/helpers/createTableHook.ts @@ -30,7 +30,7 @@ import type { TableOptions, TableState, } from '@tanstack/table-core' -import type { Type } from '@angular/core' +import type { Signal, Type } from '@angular/core' type RenderableComponent = | Type @@ -247,7 +247,20 @@ export type AppAngularTable< TTableComponents extends Record, TCellComponents extends Record, THeaderComponents extends Record, -> = AngularTable & NoInfer +> = AngularTable & + NoInfer & { + appCell: ( + cell: Cell, + ) => Cell & NoInfer + + appHeader: ( + header: Header, + ) => Header & NoInfer + + appFooter: ( + footer: Header, + ) => Header & NoInfer + } // ============================================================================= // CreateTableHook Options and Props @@ -305,7 +318,9 @@ export function createTableHook< TCellComponents, THeaderComponents >) { - function injectTableContext() { + function injectTableContext(): Signal< + AngularTable + > { return _injectTableContext() } @@ -345,9 +360,21 @@ export function createTableHook< TCellComponents, THeaderComponents > { + function appCell(cell: Cell) { + return cell as Cell & TCellComponents + } + + function appHeader(header: Cell) { + return header as Cell & TCellComponents + } + + function appFooter(footer: Cell) { + return footer as Cell & TCellComponents + } + const appTableFeatures: TableFeature<{}> = { constructTableAPIs: (table) => { - Object.assign(table, tableComponents) + Object.assign(table, tableComponents, { appCell, appHeader, appFooter }) }, assignCellPrototype(prototype) { Object.assign(prototype, cellComponents) diff --git a/packages/angular-table/src/helpers/table.ts b/packages/angular-table/src/helpers/table.ts index 4baec5264f..a5c9ff6a76 100644 --- a/packages/angular-table/src/helpers/table.ts +++ b/packages/angular-table/src/helpers/table.ts @@ -1,18 +1,12 @@ import { Directive, InjectionToken, inject, input } from '@angular/core' -import { RowData, Table, TableFeatures } from '@tanstack/table-core' +import { RowData, TableFeatures, TableState } from '@tanstack/table-core' +import { AngularTable } from '../injectTable' import type { Signal } from '@angular/core' export const TanStackTableToken = new InjectionToken< - TanStackTableContext['table'] + Signal> >('[TanStack Table] Table Context') -export interface TanStackTableContext< - TFeatures extends TableFeatures, - TData extends RowData, -> { - table: Signal> -} - @Directive({ selector: '[tanStackTable]', exportAs: 'table', @@ -26,8 +20,9 @@ export interface TanStackTableContext< export class TanStackTable< TFeatures extends TableFeatures, TData extends RowData, -> implements TanStackTableContext { - readonly table = input.required>({ + TSelected extends {} = TableState, +> { + readonly table = input.required>({ alias: 'tanStackTable', }) } @@ -35,6 +30,7 @@ export class TanStackTable< export function injectTableContext< TFeatures extends TableFeatures, TData extends RowData, ->(): TanStackTableContext['table'] { + TSelected extends {} = TableState, +>(): Signal> { return inject(TanStackTableToken) } diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index d99eb1a646..38771c9cf9 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -22,7 +22,7 @@ import type { Signal, ValueEqualityFn } from '@angular/core' export type AngularTable< TFeatures extends TableFeatures, TData extends RowData, - TSelected = {}, + TSelected = TableState, > = Table & { /** * The selected state from the table store, based on the selector provided. From 30c57f4e725c4ebd064e2d54c7794068b4e216a5 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 21:51:06 +0100 Subject: [PATCH 24/30] refactor composable table examples with products/users table components, add missing header/table components --- .../src/app/app.component.html | 66 ++----------- .../src/app/app.component.ts | 82 ++-------------- .../src/app/components/header-components.ts | 46 +++++++-- .../products-table/products-table.html | 83 ++++++++++++++++ .../products-table/products-table.ts | 68 ++++++++++++++ .../src/app/components/table-components.ts | 94 ++++++++++++++++++- .../components/users-table/users-table.html | 83 ++++++++++++++++ .../app/components/users-table/users-table.ts | 79 ++++++++++++++++ .../composable-tables/src/app/table.ts | 23 +++-- .../angular/composable-tables/src/index.html | 1 - 10 files changed, 476 insertions(+), 149 deletions(-) create mode 100644 examples/angular/composable-tables/src/app/components/products-table/products-table.html create mode 100644 examples/angular/composable-tables/src/app/components/products-table/products-table.ts create mode 100644 examples/angular/composable-tables/src/app/components/users-table/users-table.html create mode 100644 examples/angular/composable-tables/src/app/components/users-table/users-table.ts diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index 331ef7b47c..77c3d31b54 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -1,59 +1,13 @@ -
-
- +

Composable Tables Example

+

+ Both tables below use the same useAppTable hook and + shareable components, but with different data types and column + configurations. +

- - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (header of headerGroup.headers; track header.id) { - @if (!header.isPlaceholder) { - - } - } - - } - - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getAllCells(); track cell.id) { - - } - - } - + - - @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { - - @for (footer of footerGroup.headers; track footer.id) { - - } - - } - -
- - {{ header }} - -
- - {{ cell }} - -
- @if (!footer.isPlaceholder) { - - {{ footer }} - - } -
-
+
+ + -
-
diff --git a/examples/angular/composable-tables/src/app/app.component.ts b/examples/angular/composable-tables/src/app/app.component.ts index 47dfa1cd5d..a4ac466576 100644 --- a/examples/angular/composable-tables/src/app/app.component.ts +++ b/examples/angular/composable-tables/src/app/app.component.ts @@ -1,80 +1,14 @@ -import { ChangeDetectionStrategy, Component, signal } from '@angular/core' -import { - FlexRender, - TanStackTable, - TanStackTableCell, - TanStackTableHeader, - flexRenderComponent, -} from '@tanstack/angular-table' -import { NgComponentOutlet } from '@angular/common' -import { createAppColumnHelper, injectAppTable } from './table' -import { makeData } from './makeData' -import type { Person } from './makeData' - -// Create column helpers with TFeatures already bound - only need TData! -const personColumnHelper = createAppColumnHelper() +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { UsersTable } from './components/users-table/users-table' +import { ProductsTable } from './components/products-table/products-table' @Component({ selector: 'app-root', - imports: [ - FlexRender, - TanStackTableHeader, - TanStackTableCell, - NgComponentOutlet, - TanStackTable, - ], + imports: [UsersTable, ProductsTable], + host: { + class: 'app', + }, templateUrl: './app.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AppComponent { - readonly data = signal(makeData(5000)) - - readonly columns = personColumnHelper.columns([ - personColumnHelper.accessor('firstName', { - header: 'First Name', - footer: ({ header }) => flexRenderComponent(header.FooterColumnId), - cell: ({ cell }) => flexRenderComponent(cell.TextCell), - }), - personColumnHelper.accessor('lastName', { - header: 'Last Name', - footer: ({ header }) => flexRenderComponent(header.FooterColumnId), - cell: ({ cell }) => flexRenderComponent(cell.TextCell), - }), - personColumnHelper.accessor('age', { - header: 'Age', - footer: ({ header }) => flexRenderComponent(header.FooterSum), - cell: ({ cell }) => flexRenderComponent(cell.NumberCell), - }), - personColumnHelper.accessor('visits', { - header: 'Visits', - footer: ({ header }) => flexRenderComponent(header.FooterSum), - cell: ({ cell }) => flexRenderComponent(cell.NumberCell), - }), - personColumnHelper.accessor('status', { - header: 'Status', - footer: ({ header }) => flexRenderComponent(header.FooterColumnId), - cell: ({ cell }) => flexRenderComponent(cell.StatusCell), - }), - personColumnHelper.accessor('progress', { - header: 'Progress', - footer: ({ header }) => flexRenderComponent(header.FooterSum), - cell: ({ cell }) => flexRenderComponent(cell.ProgressCell), - }), - personColumnHelper.display({ - id: 'actions', - header: 'Actions', - cell: ({ cell }) => flexRenderComponent(cell.RowActionsCell), - }), - ]) - - table = injectAppTable(() => ({ - columns: this.columns, - data: this.data(), - debugTable: true, - // more table options - })) - - onRefresh = () => { - this.data.set([...makeData(5000)]) - } -} +export class AppComponent {} diff --git a/examples/angular/composable-tables/src/app/components/header-components.ts b/examples/angular/composable-tables/src/app/components/header-components.ts index 2d23c41b53..7afeaacb8e 100644 --- a/examples/angular/composable-tables/src/app/components/header-components.ts +++ b/examples/angular/composable-tables/src/app/components/header-components.ts @@ -10,18 +10,44 @@ // } import { Component, computed } from '@angular/core' +import { flexRenderComponent } from '@tanstack/angular-table' +import { FormsModule } from '@angular/forms' import { injectTableHeaderContext } from '../table' +import type { FlexRenderComponent } from '@tanstack/angular-table' -// @Component({ -// selector: 'app-sort-indicator', -// host: { -// class: 'sort-indicator', -// }, -// template: ` {{ sorted === 'asc' ? '🔼' : '🔽' }} `, -// }) -// export class SortIndicator { -// readonly context = injectTableHeaderContext() -// } +export function SortIndicator(): string | null { + const header = injectTableHeaderContext() + const sorted = header().column.getIsSorted() + if (!sorted) { + return null + } + return `${sorted === 'asc' ? '🔼' : '🔽'}` +} + +export function ColumnFilter(): FlexRenderComponent | null { + const header = injectTableHeaderContext() + if (!header().column.getCanFilter()) return null + return flexRenderComponent(_ColumnFilter) +} + +@Component({ + template: ` +
+ +
+ `, + imports: [FormsModule], +}) +export class _ColumnFilter { + readonly header = injectTableHeaderContext() + readonly filterValue = computed(() => this.header().column.getFilterValue()) + readonly placeholder = computed(() => `Filter ${this.header().column.id}...`) +} @Component({ selector: 'span', diff --git a/examples/angular/composable-tables/src/app/components/products-table/products-table.html b/examples/angular/composable-tables/src/app/components/products-table/products-table.html new file mode 100644 index 0000000000..f6310f7527 --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/products-table/products-table.html @@ -0,0 +1,83 @@ +
+ + + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (_header of headerGroup.headers; track _header.id) { + @let header = table.appHeader(_header); + @if (!header.isPlaceholder) { + + } + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + + + + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (footer of footerGroup.headers; track footer.id) { + + } + + } + +
+ + {{ header }} + + + +
+
+ + +
+
+
+ + {{ cell }} + +
+ @if (!footer.isPlaceholder) { + + {{ footer }} + + } +
+ + + + +
diff --git a/examples/angular/composable-tables/src/app/components/products-table/products-table.ts b/examples/angular/composable-tables/src/app/components/products-table/products-table.ts new file mode 100644 index 0000000000..51e780112f --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/products-table/products-table.ts @@ -0,0 +1,68 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { NgComponentOutlet } from '@angular/common' +import { + FlexRender, + TanStackTable, + TanStackTableCell, + TanStackTableHeader, +} from '@tanstack/angular-table' +import { makeProductData } from '../../makeData' +import { createAppColumnHelper, injectAppTable } from '../../table' +import type { Product } from '../../makeData' + +export const productColumnHelper = createAppColumnHelper() + +@Component({ + selector: 'products-table', + templateUrl: './products-table.html', + imports: [ + NgComponentOutlet, + FlexRender, + TanStackTable, + TanStackTableHeader, + TanStackTableCell, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductsTable { + readonly data = signal(makeProductData(5000)) + + readonly columns = productColumnHelper.columns([ + productColumnHelper.accessor('name', { + header: 'Product Name', + footer: (props) => props.column.id, + cell: ({ cell }) => cell.TextCell, + }), + productColumnHelper.accessor('category', { + header: 'Category', + footer: (props) => props.column.id, + cell: ({ cell }) => cell.CategoryCell, + }), + productColumnHelper.accessor('price', { + header: 'Price', + footer: (props) => props.column.id, + cell: ({ cell }) => cell.PriceCell, + }), + productColumnHelper.accessor('stock', { + header: 'In Stock', + footer: (props) => props.column.id, + cell: ({ cell }) => cell.NumberCell, + }), + productColumnHelper.accessor('rating', { + header: 'Rating', + footer: (props) => props.column.id, + cell: ({ cell }) => cell.ProgressCell, + }), + ]) + + table = injectAppTable(() => ({ + columns: this.columns, + data: this.data(), + getRowId: (row) => row.id, + // more table options + })) + + onRefresh = () => { + this.data.set([...makeProductData(5000)]) + } +} diff --git a/examples/angular/composable-tables/src/app/components/table-components.ts b/examples/angular/composable-tables/src/app/components/table-components.ts index fd9734be2f..3eb1f8ff39 100644 --- a/examples/angular/composable-tables/src/app/components/table-components.ts +++ b/examples/angular/composable-tables/src/app/components/table-components.ts @@ -1,7 +1,13 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core' import { injectTableContext } from '../table' @Component({ + selector: 'app-table-toolbar', template: `

{{ title() }}

@@ -25,3 +31,89 @@ export class TableToolbar { this.table().resetColumnFilters() } } + +@Component({ + selector: 'app-row-count', + template: ` +
Showing {{ length() }} of {{ rowCount() }} rows
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RowCount { + readonly table = injectTableContext() + + readonly length = computed(() => + this.table().getRowModel().rows.length.toLocaleString(), + ) + + readonly rowCount = computed(() => + this.table().getRowCount().toLocaleString(), + ) +} + +/** + * Pagination controls for the table + */ +@Component({ + selector: 'app-pagination-controls', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PaginationControls { + readonly table = injectTableContext() + + readonly pageSizes = [10, 20, 30, 40, 50] + + readonly canPreviousPage = computed(() => this.table().getCanPreviousPage()) + readonly canNextPage = computed(() => this.table().getCanNextPage()) + readonly pageIndex = computed(() => this.table().state().pagination.pageIndex) + readonly pageSize = computed(() => this.table().state().pagination.pageSize) + readonly pageCount = computed(() => + this.table().getPageCount().toLocaleString(), + ) + + onPageChange(event: Event) { + const target = event.target as HTMLInputElement + const page = target.value ? Number(target.value) - 1 : 0 + this.table().setPageIndex(page) + } + + onPageSizeChange(event: Event) { + const target = event.target as HTMLSelectElement + this.table().setPageSize(Number(target.value)) + } +} diff --git a/examples/angular/composable-tables/src/app/components/users-table/users-table.html b/examples/angular/composable-tables/src/app/components/users-table/users-table.html new file mode 100644 index 0000000000..7524781c56 --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/users-table/users-table.html @@ -0,0 +1,83 @@ +
+ + + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (_header of headerGroup.headers; track _header.id) { + @let header = table.appHeader(_header); + @if (!header.isPlaceholder) { + + } + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + + + + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (footer of footerGroup.headers; track footer.id) { + + } + + } + +
+ + {{ header }} + + + +
+
+ + +
+
+
+ + {{ cell }} + +
+ @if (!footer.isPlaceholder) { + + {{ footer }} + + } +
+ + + + +
diff --git a/examples/angular/composable-tables/src/app/components/users-table/users-table.ts b/examples/angular/composable-tables/src/app/components/users-table/users-table.ts new file mode 100644 index 0000000000..98f4153816 --- /dev/null +++ b/examples/angular/composable-tables/src/app/components/users-table/users-table.ts @@ -0,0 +1,79 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { NgComponentOutlet } from '@angular/common' +import { + FlexRender, + TanStackTable, + TanStackTableCell, + TanStackTableHeader, + flexRenderComponent, +} from '@tanstack/angular-table' +import { makeData } from '../../makeData' +import { createAppColumnHelper, injectAppTable } from '../../table' +import type { Person } from '../../makeData' + +export const personColumnHelper = createAppColumnHelper() + +@Component({ + selector: 'users-table', + templateUrl: './users-table.html', + imports: [ + NgComponentOutlet, + FlexRender, + TanStackTable, + TanStackTableHeader, + TanStackTableCell, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UsersTable { + readonly data = signal(makeData(5000)) + + readonly columns = personColumnHelper.columns([ + personColumnHelper.accessor('firstName', { + header: 'First Name', + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => flexRenderComponent(cell.TextCell), + }), + personColumnHelper.accessor('lastName', { + header: 'Last Name', + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => flexRenderComponent(cell.TextCell), + }), + personColumnHelper.accessor('age', { + header: 'Age', + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => flexRenderComponent(cell.NumberCell), + }), + personColumnHelper.accessor('visits', { + header: 'Visits', + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => flexRenderComponent(cell.NumberCell), + }), + personColumnHelper.accessor('status', { + header: 'Status', + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => flexRenderComponent(cell.StatusCell), + }), + personColumnHelper.accessor('progress', { + header: 'Progress', + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => flexRenderComponent(cell.ProgressCell), + }), + personColumnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ cell }) => flexRenderComponent(cell.RowActionsCell), + }), + ]) + + table = injectAppTable(() => ({ + columns: this.columns, + data: this.data(), + debugTable: true, + // more table options + })) + + onRefresh = () => { + this.data.set([...makeData(5000)]) + } +} diff --git a/examples/angular/composable-tables/src/app/table.ts b/examples/angular/composable-tables/src/app/table.ts index a4a6ef2bb4..00c2dd364f 100644 --- a/examples/angular/composable-tables/src/app/table.ts +++ b/examples/angular/composable-tables/src/app/table.ts @@ -10,6 +10,7 @@ import { createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, + createTableHook, filterFns, rowPaginationFeature, rowSortingFeature, @@ -17,8 +18,11 @@ import { tableFeatures, } from '@tanstack/angular-table' // Import table-level components -import { createTableHook } from '@tanstack/angular-table' -import { TableToolbar } from './components/table-components' +import { + PaginationControls, + RowCount, + TableToolbar, +} from './components/table-components' import { CategoryCell, NumberCell, @@ -28,7 +32,12 @@ import { StatusCell, TextCell, } from './components/cell-components' -import { FooterColumnId, FooterSum } from './components/header-components' +import { + ColumnFilter, + FooterColumnId, + FooterSum, + SortIndicator, +} from './components/header-components' // Import table-level components // import { @@ -80,8 +89,8 @@ export const { // Register table-level components (accessible via table.ComponentName) tableComponents: { - // PaginationControls, - // RowCount, + PaginationControls, + RowCount, TableToolbar, }, @@ -98,8 +107,8 @@ export const { // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) headerComponents: { - // SortIndicator, - // ColumnFilter, + SortIndicator, + ColumnFilter, FooterColumnId, FooterSum, }, diff --git a/examples/angular/composable-tables/src/index.html b/examples/angular/composable-tables/src/index.html index 9a37746785..2f15880fea 100644 --- a/examples/angular/composable-tables/src/index.html +++ b/examples/angular/composable-tables/src/index.html @@ -6,7 +6,6 @@ - From 9d782a83bc1c0a4fcbfc564796a0995283004f8f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:52:00 +0000 Subject: [PATCH 25/30] ci: apply automated fixes --- .../src/app/app.component.html | 10 +-- .../products-table/products-table.html | 90 +++++++++---------- .../components/users-table/users-table.html | 90 +++++++++---------- 3 files changed, 92 insertions(+), 98 deletions(-) diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index 77c3d31b54..cf20b66f5f 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -1,13 +1,11 @@

Composable Tables Example

- Both tables below use the same useAppTable hook and - shareable components, but with different data types and column - configurations. + Both tables below use the same useAppTable hook and shareable + components, but with different data types and column configurations.

- +
- - + diff --git a/examples/angular/composable-tables/src/app/components/products-table/products-table.html b/examples/angular/composable-tables/src/app/components/products-table/products-table.html index f6310f7527..03bf24a5b8 100644 --- a/examples/angular/composable-tables/src/app/components/products-table/products-table.html +++ b/examples/angular/composable-tables/src/app/components/products-table/products-table.html @@ -9,75 +9,73 @@ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (_header of headerGroup.headers; track _header.id) { - @let header = table.appHeader(_header); - @if (!header.isPlaceholder) { - + @for (_header of headerGroup.headers; track _header.id) { @let header = + table.appHeader(_header); @if (!header.isPlaceholder) { + - } - } - + > +
+ + + } } + } @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getAllCells(); track cell.id) { - - } - + + @for (cell of row.getAllCells(); track cell.id) { + + } + } @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { - - @for (footer of footerGroup.headers; track footer.id) { - + + @for (footer of footerGroup.headers; track footer.id) { + + + } + }
- - {{ header }} - +
+ + {{ header }} + - -
-
+ > +
+ - -
-
-
- - {{ cell }} - -
+ + {{ cell }} + +
- @if (!footer.isPlaceholder) { - - {{ footer }} - - } -
+ @if (!footer.isPlaceholder) { + + {{ footer }} + } -
- + - +
diff --git a/examples/angular/composable-tables/src/app/components/users-table/users-table.html b/examples/angular/composable-tables/src/app/components/users-table/users-table.html index 7524781c56..ad811b75d9 100644 --- a/examples/angular/composable-tables/src/app/components/users-table/users-table.html +++ b/examples/angular/composable-tables/src/app/components/users-table/users-table.html @@ -9,75 +9,73 @@ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (_header of headerGroup.headers; track _header.id) { - @let header = table.appHeader(_header); - @if (!header.isPlaceholder) { - + @for (_header of headerGroup.headers; track _header.id) { @let header = + table.appHeader(_header); @if (!header.isPlaceholder) { + - } - } - + > +
+ + + } } + } @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getAllCells(); track cell.id) { - - } - + + @for (cell of row.getAllCells(); track cell.id) { + + } + } @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { - - @for (footer of footerGroup.headers; track footer.id) { - + + @for (footer of footerGroup.headers; track footer.id) { + + + } + }
- - {{ header }} - +
+ + {{ header }} + - -
-
+ > +
+ - -
-
-
- - {{ cell }} - -
+ + {{ cell }} + +
- @if (!footer.isPlaceholder) { - - {{ footer }} - - } -
+ @if (!footer.isPlaceholder) { + + {{ footer }} + } -
- + - +
From e6315c305cf11d860bf0e5bb3661caac8343c24e Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 22:45:18 +0100 Subject: [PATCH 26/30] refactor table components to use inject functions and update documentation --- .../src/app/app.component.html | 5 +- .../composable-tables/src/app/table.ts | 30 +++---- .../src/helpers/createTableHook.ts | 80 ++++++++++++++++--- 3 files changed, 83 insertions(+), 32 deletions(-) diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index cf20b66f5f..f83b366762 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -1,7 +1,8 @@

Composable Tables Example

- Both tables below use the same useAppTable hook and shareable - components, but with different data types and column configurations. + Both tables below use the same injectAppTable function and + shareable components, but with different data types and column + configurations.

diff --git a/examples/angular/composable-tables/src/app/table.ts b/examples/angular/composable-tables/src/app/table.ts index 00c2dd364f..4bcb3a7169 100644 --- a/examples/angular/composable-tables/src/app/table.ts +++ b/examples/angular/composable-tables/src/app/table.ts @@ -17,12 +17,14 @@ import { sortFns, tableFeatures, } from '@tanstack/angular-table' + // Import table-level components import { PaginationControls, RowCount, TableToolbar, } from './components/table-components' +// Import cell-level components import { CategoryCell, NumberCell, @@ -32,6 +34,7 @@ import { StatusCell, TextCell, } from './components/cell-components' +// Import header-level components (both use injectTableHeaderContext()) import { ColumnFilter, FooterColumnId, @@ -39,25 +42,16 @@ import { SortIndicator, } from './components/header-components' -// Import table-level components -// import { -// PaginationControls, -// RowCount, -// TableToolbar, -// } from '../components/table-components' - -// Import cell-level components - -// Import header/footer-level components (both use useHeaderContext) - /** * Create the custom table hook with all pre-bound components. * This exports: * - createAppColumnHelper: Create column definitions with TFeatures already bound - * - useAppTable: Hook for creating tables with TFeatures baked in - * - useTableContext: Access table instance in tableComponents - * - useCellContext: Access cell instance in cellComponents - * - useHeaderContext: Access header instance in headerComponents + * - injectAppTable: Function for creating tables with TFeatures baked in + * - injectTableContext: Access table instance in tableComponents + * - injectTableCellContext: Access cell instance in cellComponents + * - injectTableHeaderContext: Access header instance in headerComponents + * - injectFlexRenderHeaderContext: Access FlexRenderContext with header-level typings + * - injectFlexRenderCellContext: Access FlexRenderContext with header-level typings */ export const { createAppColumnHelper, @@ -65,10 +59,8 @@ export const { injectTableContext, injectTableCellContext, injectTableHeaderContext, - // useAppTable, - // useTableContext, - // useCellContext, - // useHeaderContext, + // injectFlexRenderHeaderContext + // injectFlexRenderCellContext } = createTableHook({ // Features are set once here and shared across all tables _features: tableFeatures({ diff --git a/packages/angular-table/src/helpers/createTableHook.ts b/packages/angular-table/src/helpers/createTableHook.ts index bff4da52e6..03c6fddf94 100644 --- a/packages/angular-table/src/helpers/createTableHook.ts +++ b/packages/angular-table/src/helpers/createTableHook.ts @@ -302,6 +302,53 @@ export type CreateTableContextOptions< headerComponents?: THeaderComponents } +export type CreateTableHookResult< + TFeatures extends TableFeatures, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +> = { + createAppColumnHelper: () => AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > + injectTableContext: () => Signal< + AngularTable + > + injectTableHeaderContext: < + TValue extends CellData = CellData, + TRowData extends RowData = RowData, + >() => Signal> + injectTableCellContext: < + TValue extends CellData = CellData, + TRowData extends RowData = RowData, + >() => Signal> + injectFlexRenderHeaderContext: < + TData extends RowData, + TValue extends CellData, + >() => HeaderContext + injectFlexRenderCellContext: < + TData extends RowData, + TValue extends CellData, + >() => CellContext + injectAppTable: ( + tableOptions: () => Omit< + TableOptions, + '_features' | '_rowModels' + >, + selector?: (state: TableState) => TSelected, + ) => AppAngularTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > +} + export function createTableHook< TFeatures extends TableFeatures, const TTableComponents extends Record, @@ -317,32 +364,43 @@ export function createTableHook< TTableComponents, TCellComponents, THeaderComponents ->) { +>): CreateTableHookResult< + TFeatures, + TTableComponents, + TCellComponents, + THeaderComponents +> { function injectTableContext(): Signal< AngularTable > { return _injectTableContext() } - function injectTableHeaderContext() { - return _injectTableHeaderContext() + function injectTableHeaderContext< + TValue extends CellData = CellData, + TRowData extends RowData = RowData, + >(): Signal> { + return _injectTableHeaderContext() } - function injectTableCellContext() { - return _injectTableCellContext() + function injectTableCellContext< + TValue extends CellData = CellData, + TRowData extends RowData = RowData, + >(): Signal> { + return _injectTableCellContext() } function injectFlexRenderHeaderContext< TData extends RowData, TValue extends CellData, - >() { + >(): HeaderContext { return injectFlexRenderContext>() } function injectFlexRenderCellContext< TData extends RowData, TValue extends CellData, - >() { + >(): CellContext { return injectFlexRenderContext>() } @@ -364,12 +422,12 @@ export function createTableHook< return cell as Cell & TCellComponents } - function appHeader(header: Cell) { - return header as Cell & TCellComponents + function appHeader(header: Header) { + return header as Header & THeaderComponents } - function appFooter(footer: Cell) { - return footer as Cell & TCellComponents + function appFooter(footer: Header) { + return footer as Header & THeaderComponents } const appTableFeatures: TableFeature<{}> = { From 6c198503b58ce2a26a3b476f1cf4d0a611718d96 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:46:58 +0000 Subject: [PATCH 27/30] ci: apply automated fixes --- examples/angular/composable-tables/src/app/app.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/angular/composable-tables/src/app/app.component.html b/examples/angular/composable-tables/src/app/app.component.html index f83b366762..a55e45ad8b 100644 --- a/examples/angular/composable-tables/src/app/app.component.html +++ b/examples/angular/composable-tables/src/app/app.component.html @@ -1,8 +1,7 @@

Composable Tables Example

Both tables below use the same injectAppTable function and - shareable components, but with different data types and column - configurations. + shareable components, but with different data types and column configurations.

From 43014d5fc24e99e39ace2032950bac2ec85411e8 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 22:48:12 +0100 Subject: [PATCH 28/30] fix: update comment to reflect usage of injectTable hook in app.component.ts --- examples/angular/basic/src/app/app.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/angular/basic/src/app/app.component.ts b/examples/angular/basic/src/app/app.component.ts index eddc314cfb..d2259c159e 100644 --- a/examples/angular/basic/src/app/app.component.ts +++ b/examples/angular/basic/src/app/app.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table' import type { ColumnDef } from '@tanstack/angular-table' -// This example uses the classic standalone `useTable` hook to create a table without the new `createTableHelper` util. +// This example uses the classic standalone `injectTable` hook to create a table without the new `createTableHelper` util. // 1. Define what the shape of your data will be for each row type Person = { From 670de065caf16c2391ed5e346844588ca7dbf0d1 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 22:55:27 +0100 Subject: [PATCH 29/30] fix: update documentation for injectFlexRenderCellContext to clarify cell-level typings --- examples/angular/composable-tables/src/app/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/angular/composable-tables/src/app/table.ts b/examples/angular/composable-tables/src/app/table.ts index 4bcb3a7169..c898eb0a92 100644 --- a/examples/angular/composable-tables/src/app/table.ts +++ b/examples/angular/composable-tables/src/app/table.ts @@ -51,7 +51,7 @@ import { * - injectTableCellContext: Access cell instance in cellComponents * - injectTableHeaderContext: Access header instance in headerComponents * - injectFlexRenderHeaderContext: Access FlexRenderContext with header-level typings - * - injectFlexRenderCellContext: Access FlexRenderContext with header-level typings + * - injectFlexRenderCellContext: Access FlexRenderContext with cell-level typings */ export const { createAppColumnHelper, From 92f0d470bf1936db311461c43710cca3d63ba686 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 12 Jan 2026 23:04:58 +0100 Subject: [PATCH 30/30] fix: update RowActionsCell to use typed injectTableCellContext with Person type --- .../composable-tables/src/app/components/cell-components.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/angular/composable-tables/src/app/components/cell-components.ts b/examples/angular/composable-tables/src/app/components/cell-components.ts index a6c20ad3a6..cf539bda14 100644 --- a/examples/angular/composable-tables/src/app/components/cell-components.ts +++ b/examples/angular/composable-tables/src/app/components/cell-components.ts @@ -3,6 +3,7 @@ import { injectFlexRenderContext } from '@tanstack/angular-table' import { CurrencyPipe } from '@angular/common' import { injectTableCellContext } from '../table' import type { CellContext, TableFeatures } from '@tanstack/angular-table' +import type { Person } from '../makeData' @Component({ selector: 'span', @@ -67,7 +68,7 @@ export class ProgressCell { `, }) export class RowActionsCell { - readonly cell = injectTableCellContext() + readonly cell = injectTableCellContext() view() { alert(