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 @@
+
+
+
+ @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 0000000000..57614f9c96
Binary files /dev/null and b/examples/angular/basic-app-table/src/favicon.ico differ
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.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) {
- |
-
- {{ footer }}
-
+ |
+ {{ footer }}
|
}
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/examples/angular/column-ordering/src/app/app.component.ts b/examples/angular/column-ordering/src/app/app.component.ts
index 09918b53e8..66220503fa 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)
@@ -113,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 f3a6a4cb58..e92c532dec 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,
@@ -112,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 83cebb4cb2..c01ed66eeb 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)
@@ -136,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-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/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/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..331ef7b47c
--- /dev/null
+++ b/examples/angular/composable-tables/src/app/app.component.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
+
+ @for (header of headerGroup.headers; track header.id) {
+ @if (!header.isPlaceholder) {
+ |
+
+ {{ header }}
+
+ |
+ }
+ }
+
+ }
+
+
+ @for (row of table.getRowModel().rows; track row.id) {
+
+ @for (cell of row.getAllCells(); track cell.id) {
+ |
+
+ {{ cell }}
+
+ |
+ }
+
+ }
+
+
+
+ @for (footerGroup of table.getFooterGroups(); track footerGroup.id) {
+
+ @for (footer of footerGroup.headers; track footer.id) {
+ |
+ @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
new file mode 100644
index 0000000000..47dfa1cd5d
--- /dev/null
+++ b/examples/angular/composable-tables/src/app/app.component.ts
@@ -0,0 +1,80 @@
+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()
+
+@Component({
+ selector: 'app-root',
+ imports: [
+ FlexRender,
+ TanStackTableHeader,
+ TanStackTableCell,
+ 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: ({ 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/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/cell-components.ts b/examples/angular/composable-tables/src/app/components/cell-components.ts
new file mode 100644
index 0000000000..a6c20ad3a6
--- /dev/null
+++ b/examples/angular/composable-tables/src/app/components/cell-components.ts
@@ -0,0 +1,113 @@
+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-row-actions',
+ 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: ` {{ cell().getValue() | currency }} `,
+ imports: [CurrencyPipe],
+})
+export class PriceCell {
+ readonly cell = injectTableCellContext()
+}
+
+@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
new file mode 100644
index 0000000000..2d23c41b53
--- /dev/null
+++ b/examples/angular/composable-tables/src/app/components/header-components.ts
@@ -0,0 +1,58 @@
+// export function SortIndicator() {
+// const header = useHeaderContext()
+// const sorted = header.column.getIsSorted()
+//
+// if (!sorted) return null
+//
+// return (
+// {sorted === 'asc' ? '🔼' : '🔽'}
+// )
+// }
+
+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: {
+ 'tanstack-footer-column-id': '',
+ class: 'footer-column-id',
+ },
+ template: `{{ header().column.id }}`,
+})
+export class FooterColumnId {
+ readonly header = injectTableHeaderContext()
+}
+
+@Component({
+ selector: 'span',
+ host: {
+ 'tanstack-footer-sum': '',
+ class: 'footer-sum',
+ },
+ template: `{{ sum() > 0 ? sum().toLocaleString() : '—' }}`,
+})
+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
new file mode 100644
index 0000000000..fd9734be2f
--- /dev/null
+++ b/examples/angular/composable-tables/src/app/components/table-components.ts
@@ -0,0 +1,27 @@
+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 table = injectTableContext()
+
+ constructor() {
+ this.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..a4a6ef2bb4
--- /dev/null
+++ b/examples/angular/composable-tables/src/app/table.ts
@@ -0,0 +1,106 @@
+/**
+ * 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,
+ filterFns,
+ rowPaginationFeature,
+ rowSortingFeature,
+ sortFns,
+ 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 {
+// 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,
+ injectTableCellContext,
+ injectTableHeaderContext,
+ // useAppTable,
+ // useTableContext,
+ // useCellContext,
+ // useHeaderContext,
+} = createTableHook({
+ // 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,
+ ProgressCell,
+ StatusCell,
+ CategoryCell,
+ PriceCell,
+ RowActionsCell,
+ },
+
+ // 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 0000000000..57614f9c96
Binary files /dev/null and b/examples/angular/composable-tables/src/favicon.ico differ
diff --git a/examples/angular/composable-tables/src/index.html b/examples/angular/composable-tables/src/index.html
new file mode 100644
index 0000000000..9a37746785
--- /dev/null
+++ b/examples/angular/composable-tables/src/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Composable tables
+
+
+
+
+
+
+
+
+
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..d7a5ed6c49
--- /dev/null
+++ b/examples/angular/composable-tables/src/styles.scss
@@ -0,0 +1,249 @@
+html {
+ font-family: sans-serif;
+ font-size: 14px;
+}
+
+table {
+ border: 1px solid lightgray;
+ border-collapse: collapse;
+ width: 100%;
+}
+
+tbody {
+ border-bottom: 1px solid lightgray;
+}
+
+th,
+td {
+ border-bottom: 1px solid lightgray;
+ border-right: 1px solid lightgray;
+ padding: 8px 12px;
+ text-align: left;
+}
+
+th {
+ background-color: #f5f5f5;
+ font-weight: 600;
+}
+
+tfoot {
+ color: gray;
+}
+
+tfoot th {
+ font-weight: normal;
+}
+
+.table-container {
+ padding: 16px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.pagination {
+ display: flex;
+ 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;
+}
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/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/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.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) {
@if (!header.isPlaceholder) {
-
+
{{ headerCell }}
@@ -38,13 +32,7 @@
@for (cell of row.getAllCells(); track cell.id) {
|
-
+
{{ renderCell }}
|
diff --git a/examples/angular/row-selection/src/app/app.component.ts b/examples/angular/row-selection/src/app/app.component.ts
index bfcc77b615..f2a28d09ba 100644
--- a/examples/angular/row-selection/src/app/app.component.ts
+++ b/examples/angular/row-selection/src/app/app.component.ts
@@ -6,7 +6,7 @@ import {
viewChild,
} from '@angular/core'
import {
- FlexRenderDirective,
+ FlexRender,
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
@@ -23,9 +23,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 +43,7 @@ const tableHelper = createTableHelper({
@Component({
selector: 'app-root',
standalone: true,
- imports: [FilterComponent, FlexRenderDirective, FormsModule],
+ imports: [FilterComponent, FlexRender, FormsModule],
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
@@ -58,12 +58,8 @@ export class AppComponent {
readonly columns: Array> = [
{
id: 'select',
- header: () => {
- return flexRenderComponent(TableHeadSelectionComponent)
- },
- cell: () => {
- return flexRenderComponent(TableRowSelectionComponent)
- },
+ header: () => flexRenderComponent(TableHeadSelectionComponent),
+ cell: () => flexRenderComponent(TableRowSelectionComponent),
},
{
header: 'Name',
@@ -73,7 +69,7 @@ export class AppComponent {
accessorKey: 'firstName',
cell: (info) => info.getValue(),
footer: (props) => props.column.id,
- header: 'First name',
+ header: (props) => `First name`,
},
{
accessorFn: (row) => row.lastName,
@@ -123,7 +119,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) => {
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)
diff --git a/packages/angular-table/package.json b/packages/angular-table/package.json
index 97d583fea8..e4220ec4a5 100644
--- a/packages/angular-table/package.json
+++ b/packages/angular-table/package.json
@@ -33,14 +33,14 @@
}
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
},
"files": [
"dist",
"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",
@@ -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/angularReactivityFeature.ts b/packages/angular-table/src/angularReactivityFeature.ts
index ffbbd4e54f..8e5453fd9f 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
}
@@ -67,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/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 8d115dfcba..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
@@ -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,
@@ -22,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/flex-render/view.ts b/packages/angular-table/src/flex-render/view.ts
index fbdda58851..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' }
@@ -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/flex-render.ts b/packages/angular-table/src/flexRender.ts
similarity index 57%
rename from packages/angular-table/src/flex-render.ts
rename to packages/angular-table/src/flexRender.ts
index e699a81210..197857e5ee 100644
--- a/packages/angular-table/src/flex-render.ts
+++ b/packages/angular-table/src/flexRender.ts
@@ -1,10 +1,9 @@
import {
- ChangeDetectorRef,
+ DestroyRef,
Directive,
- DoCheck,
+ EffectRef,
Injector,
- OnChanges,
- SimpleChanges,
+ InputSignal,
TemplateRef,
Type,
ViewContainerRef,
@@ -12,7 +11,9 @@ import {
effect,
inject,
input,
+ linkedSignal,
runInInjectionContext,
+ untracked,
} from '@angular/core'
import { FlexRenderComponentProps } from './flex-render/context'
import { FlexRenderFlags } from './flex-render/flags'
@@ -27,14 +28,17 @@ import {
FlexRenderView,
mapToFlexRenderTypedContent,
} from './flex-render/view'
+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,
+ CellData,
HeaderContext,
- Table,
+ RowData,
TableFeatures,
} from '@tanstack/table-core'
-import type { EffectRef } from '@angular/core'
export {
injectFlexRenderContext,
@@ -51,49 +55,49 @@ export type FlexRenderContent> =
| Record
| undefined
+export type FlexRenderInputContent> =
+ | number
+ | string
+ | ((props: TProps) => FlexRenderContent)
+ | null
+ | undefined
+
@Directive({
selector: 'ng-template[flexRender]',
standalone: true,
- providers: [FlexRenderComponentFactory],
})
-export class FlexRender<
+export class FlexRenderDirective<
+ TFeatures extends TableFeatures,
+ TRowData extends RowData,
+ TValue extends CellData,
TProps extends
| NonNullable
- | CellContext
- | HeaderContext,
->
- implements OnChanges, DoCheck
-{
- readonly #flexRenderComponentFactory = inject(FlexRenderComponentFactory)
- readonly #changeDetectorRef = inject(ChangeDetectorRef)
-
- readonly content = input.required<
- | number
- | string
- | ((props: TProps) => FlexRenderContent)
- | null
- | undefined
- >({
- alias: 'flexRender',
- })
-
- readonly props = input.required({
+ | CellContext
+ | HeaderContext,
+> {
+ readonly #flexRenderComponentFactory = new FlexRenderComponentFactory(
+ inject(ViewContainerRef),
+ )
+
+ readonly inputContent: InputSignal> = input(
+ undefined as FlexRenderInputContent,
+ { alias: 'flexRender' },
+ )
+ readonly content = linkedSignal(() => this.inputContent())
+
+ readonly inputProps = input({} as TProps, {
alias: 'flexRenderProps',
})
+ readonly props = linkedSignal(() => this.inputProps())
- readonly notifier = input<'doCheck' | 'tableChange'>('tableChange', {
- alias: 'flexRenderNotifier',
- })
-
- readonly injector = input(inject(Injector), {
+ readonly inputInjector = input(inject(Injector), {
alias: 'flexRenderInjector',
})
+ readonly injector = linkedSignal(() => this.inputInjector())
- readonly viewContainerRef = inject(ViewContainerRef)
-
- readonly templateRef = inject(TemplateRef)
+ readonly #viewContainerRef = inject(ViewContainerRef)
+ readonly #templateRef = inject(TemplateRef)
- table: Table
renderFlags = FlexRenderFlags.ViewFirstRender
renderView: FlexRenderView | null = null
@@ -112,32 +116,44 @@ export class FlexRender<
return mapToFlexRenderTypedContent(latestContent)
})
- ngOnChanges(changes: SimpleChanges>) {
- 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']) {
- this.renderFlags |=
- FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender
- this.update()
- }
- }
+ 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
+ }
+ })
- 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
- }
+ let previousContent: FlexRenderInputContent
+ let previousProps: TProps
- if (this.notifier() === 'doCheck') {
- this.renderFlags |= FlexRenderFlags.DirtyCheck
- this.doCheck()
- }
+ effect(() => {
+ const props = this.props()
+ const content = this.content()
+
+ if (!(this.renderFlags & FlexRenderFlags.ViewFirstRender)) {
+ if (previousContent !== content) {
+ this.renderFlags |= FlexRenderFlags.ContentChanged
+ }
+ if (previousProps !== props) {
+ this.renderFlags |= FlexRenderFlags.PropsReferenceChanged
+ }
+ }
+
+ untracked(() => this.update())
+
+ if (FlexRenderFlags.ViewFirstRender & this.renderFlags) {
+ this.renderFlags &= ~FlexRenderFlags.ViewFirstRender
+ }
+
+ previousContent = content
+ previousProps = props
+ })
}
private doCheck() {
@@ -154,28 +170,6 @@ 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') {
- this.#tableChangeEffect = effect(
- () => {
- this.table.get()
- if (firstCheck) {
- firstCheck = false
- return
- }
- this.renderFlags |= FlexRenderFlags.DirtyCheck
- this.doCheck()
- },
- { injector: this.injector() },
- )
- }
- }
-
update() {
if (
this.renderFlags &
@@ -184,46 +178,49 @@ export class FlexRender<
this.render()
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.
- if (!this.#currentEffectRef && typeof this.content === 'function') {
+ // 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 untracked(this.content) === 'function'
+ ) {
this.#currentEffectRef = effect(
() => {
this.#latestContent()
@@ -231,12 +228,10 @@ export class FlexRender<
this.renderFlags |= FlexRenderFlags.RenderEffectChecked
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.renderFlags |= FlexRenderFlags.Dirty
+ this.doCheck()
},
- { injector: this.viewContainerRef.injector },
+ { injector: this.#viewContainerRef.injector },
)
}
}
@@ -274,7 +269,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()
},
@@ -286,11 +281,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)
}
@@ -300,16 +299,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.#getInjector(injector)
const view = this.#flexRenderComponentFactory.createComponent(
flexRenderComponent.content,
componentInjector,
@@ -320,19 +311,49 @@ export class FlexRender<
#renderCustomComponent(
component: Extract,
): FlexRenderComponentView {
+ const instance = flexRenderComponent(component.content, {
+ inputs: this.props(),
+ injector: this.#getInjector(this.injector()),
+ })
const view = this.#flexRenderComponentFactory.createComponent(
- flexRenderComponent(component.content, {
- inputs: this.props(),
- injector: this.injector(),
- }),
+ instance,
this.injector(),
)
return new FlexRenderComponentView(component, view)
}
-}
-/**
- * @deprecated Use `FlexRender` import instead.
- * @alias FlexRender
- */
-export const FlexRenderDirective = FlexRender
+ #getInjector(parentInjector?: Injector) {
+ const getContext = () => this.props()
+ const proxy = new Proxy(this.props(), {
+ get: (_, key) => getContext()[key as keyof typeof _],
+ })
+
+ const staticProviders = []
+ if ('table' in proxy) {
+ staticProviders.push({
+ provide: TanStackTableToken,
+ useValue: () => proxy.table,
+ })
+ }
+ if ('cell' in proxy) {
+ staticProviders.push({
+ provide: TanStackTableCellToken,
+ useValue: () => proxy.cell,
+ })
+ }
+ if ('header' in proxy) {
+ staticProviders.push({
+ provide: TanStackTableHeaderToken,
+ useValue: () => proxy.header,
+ })
+ }
+
+ return Injector.create({
+ parent: parentInjector ?? this.injector(),
+ providers: [
+ ...staticProviders,
+ { provide: FlexRenderComponentProps, useValue: proxy },
+ ],
+ })
+ }
+}
diff --git a/packages/angular-table/src/helpers/cell.ts b/packages/angular-table/src/helpers/cell.ts
new file mode 100644
index 0000000000..b54c98da67
--- /dev/null
+++ b/packages/angular-table/src/helpers/cell.ts
@@ -0,0 +1,43 @@
+import { Directive, InjectionToken, 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>
+}
+
+export const TanStackTableCellToken = new InjectionToken<
+ TanStackTableCellContext['cell']
+>('[TanStack Table] CellContext')
+
+@Directive({
+ selector: '[tanStackTableCell]',
+ exportAs: 'cell',
+ providers: [
+ {
+ provide: TanStackTableCellToken,
+ useFactory: () => inject(TanStackTableCell).cell,
+ },
+ ],
+})
+export class TanStackTableCell<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+ TValue extends CellData,
+> implements TanStackTableCellContext {
+ readonly cell = input.required>({
+ alias: 'tanStackTableCell',
+ })
+}
+
+export function injectTableCellContext<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+ TValue extends CellData,
+>(): TanStackTableCellContext['cell'] {
+ return inject(TanStackTableCellToken)
+}
diff --git a/packages/angular-table/src/helpers/createTableHook.ts b/packages/angular-table/src/helpers/createTableHook.ts
new file mode 100644
index 0000000000..55a2442b39
--- /dev/null
+++ b/packages/angular-table/src/helpers/createTableHook.ts
@@ -0,0 +1,395 @@
+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 _injectTableContext } from './table'
+import { injectTableCellContext as _injectTableCellContext } from './cell'
+import type { FlexRenderContent } from '../flexRender'
+import type { AngularTable } from '../injectTable'
+import type {
+ AccessorFn,
+ AccessorFnColumnDef,
+ AccessorKeyColumnDef,
+ Cell,
+ CellContext,
+ CellData,
+ Column,
+ ColumnDef,
+ DeepKeys,
+ DeepValue,
+ DisplayColumnDef,
+ GroupColumnDef,
+ Header,
+ HeaderContext,
+ IdentifiedColumnDef,
+ Row,
+ RowData,
+ Table,
+ TableFeature,
+ TableFeatures,
+ TableOptions,
+ TableState,
+} from '@tanstack/table-core'
+import type { Type } from '@angular/core'
+
+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
+}
+
+export function createTableHook<
+ TFeatures extends TableFeatures,
+ const TTableComponents extends Record,
+ const TCellComponents extends Record,
+ const THeaderComponents extends Record,
+>({
+ tableComponents,
+ cellComponents,
+ headerComponents,
+ ...defaultTableOptions
+}: CreateTableContextOptions<
+ TFeatures,
+ TTableComponents,
+ TCellComponents,
+ THeaderComponents
+>) {
+ function injectTableContext() {
+ return _injectTableContext()
+ }
+
+ function injectTableHeaderContext() {
+ return _injectTableHeaderContext()
+ }
+
+ function injectTableCellContext() {
+ 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,
+ '_features' | '_rowModels'
+ >,
+ selector?: (state: TableState) => TSelected,
+ ): AppAngularTable<
+ TFeatures,
+ TData,
+ TSelected,
+ TTableComponents,
+ TCellComponents,
+ THeaderComponents
+ > {
+ const appTableFeatures: TableFeature<{}> = {
+ constructTableAPIs: (table) => {
+ Object.assign(table, tableComponents)
+ },
+ assignCellPrototype(prototype) {
+ Object.assign(prototype, cellComponents)
+ },
+ assignHeaderPrototype(prototype) {
+ Object.assign(prototype, headerComponents)
+ },
+ }
+
+ return injectTable(() => {
+ const options = {
+ ...defaultTableOptions,
+ ...tableOptions(),
+ } as TableOptions
+ options._features = { ...options._features, appTableFeatures }
+ return options
+ }, selector) as AngularTable
+ }
+
+ 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,
+ injectFlexRenderHeaderContext,
+ injectFlexRenderCellContext,
+ injectAppTable,
+ }
+}
diff --git a/packages/angular-table/src/helpers/flexRenderCell.ts b/packages/angular-table/src/helpers/flexRenderCell.ts
new file mode 100644
index 0000000000..be575621ae
--- /dev/null
+++ b/packages/angular-table/src/helpers/flexRenderCell.ts
@@ -0,0 +1,83 @@
+import { Directive, effect, inject, input } from '@angular/core'
+import {
+ Cell,
+ CellData,
+ Header,
+ RowData,
+ TableFeatures,
+} from '@tanstack/table-core'
+import { FlexRenderDirective } from '../flexRender'
+
+/**
+ * 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
+ * | {{cell}} |
+ * {{header}} |
+ * {{footer}} |
+ * ```
+ *
+ * This replaces calling `*flexRender` directly like this:
+ * ```html
+ * {{cell}} |
+ * {{header}} |
+ * {{footer}} |
+ * ```
+ *
+ * @see {FlexRender}
+ */
+@Directive({
+ selector:
+ 'ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]',
+ hostDirectives: [{ directive: FlexRenderDirective }],
+})
+export class FlexRenderCell<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+ TValue extends CellData,
+> {
+ readonly #flexRender = inject(
+ FlexRenderDirective,
+ )
+
+ 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 } = this.#flexRender
+
+ if (cell) {
+ content.set(cell.column.columnDef.cell)
+ props.set(cell.getContext())
+ }
+
+ if (header) {
+ content.set(header.column.columnDef.header)
+ props.set(header.getContext())
+ }
+
+ if (footer) {
+ content.set(footer.column.columnDef.footer)
+ props.set(footer.getContext())
+ }
+ })
+ }
+}
diff --git a/packages/angular-table/src/helpers/header.ts b/packages/angular-table/src/helpers/header.ts
new file mode 100644
index 0000000000..d80e302ad9
--- /dev/null
+++ b/packages/angular-table/src/helpers/header.ts
@@ -0,0 +1,43 @@
+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 TanStackTableHeaderToken = new InjectionToken<
+ TanStackTableHeaderContext['header']
+>('[TanStack Table] HeaderContext')
+
+export interface TanStackTableHeaderContext<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+ TValue extends CellData,
+> {
+ header: Signal>
+}
+
+@Directive({
+ selector: '[tanStackTableHeader]',
+ exportAs: 'header',
+ providers: [
+ {
+ provide: TanStackTableHeaderToken,
+ useFactory: () => inject(TanStackTableHeader).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['header'] {
+ return inject(TanStackTableHeaderToken)
+}
diff --git a/packages/angular-table/src/helpers/table.ts b/packages/angular-table/src/helpers/table.ts
new file mode 100644
index 0000000000..4baec5264f
--- /dev/null
+++ b/packages/angular-table/src/helpers/table.ts
@@ -0,0 +1,40 @@
+import { Directive, InjectionToken, inject, input } from '@angular/core'
+import { RowData, Table, TableFeatures } from '@tanstack/table-core'
+import type { Signal } from '@angular/core'
+
+export const TanStackTableToken = new InjectionToken<
+ TanStackTableContext['table']
+>('[TanStack Table] Table Context')
+
+export interface TanStackTableContext<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+> {
+ table: Signal>
+}
+
+@Directive({
+ selector: '[tanStackTable]',
+ exportAs: 'table',
+ providers: [
+ {
+ provide: TanStackTableToken,
+ useFactory: () => inject(TanStackTable).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['table'] {
+ return inject(TanStackTableToken)
+}
diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts
index 30cd0760fc..7045d58ba8 100644
--- a/packages/angular-table/src/index.ts
+++ b/packages/angular-table/src/index.ts
@@ -1,9 +1,20 @@
+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 './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/src/injectTable.ts b/packages/angular-table/src/injectTable.ts
index 9c3bf7e99f..d99eb1a646 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,18 +33,18 @@ export type AngularTable<
*/
Subscribe: (props: {
selector: (state: TableState) => TSubSelected
- children: ((state: Signal>) => any) | any
- }) => any
+ equal?: ValueEqualityFn
+ }) => Signal>
}
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)
@@ -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/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts
index eb02ce9906..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
}
/**
@@ -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
@@ -91,7 +89,7 @@ export function toComputed<
fn: TFunction,
debugName: string,
): ComputedFunction {
- const hasArgs = fn.length > 0
+ const hasArgs = getFnArgsLength(fn) > 0
if (!hasArgs) {
const computedFn = computed(
() => {
@@ -105,23 +103,37 @@ export function toComputed<
return computedFn as ComputedFunction
}
- const computedCache: Record> = {}
-
- const computedFn = (arg0: any, ...otherArgs: Array) => {
- const argsArray = [arg0, ...otherArgs]
+ 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.hasOwnProperty(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()
}
@@ -136,28 +148,43 @@ function serializeArgs(...args: Array) {
return JSON.stringify(args)
}
+function getFnArgsLength(
+ fn: ((...args: any) => any) & { originalArgsLength?: number },
+): number {
+ return Math.max(0, getMemoFnMeta(fn)?.originalArgsLength ?? fn.length)
+}
+
export function assignReactivePrototypeAPI(
notifier: Signal,
prototype: Record,
fnName: string,
) {
+ if (isReactive(prototype[fnName])) return
+
const fn = prototype[fnName]
- const originalArgsLength = Math.max(
- 0,
- Reflect.get(fn, 'originalArgsLength') ?? 0,
- )
+ const originalArgsLength = getFnArgsLength(fn)
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)
- }))
+ // Create a cache in the current prototype to allow the signals
+ // to be garbage collected. Shorthand for a WeakMap implementation
+ self._reactiveCache ??= {}
+ const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= computed(
+ () => {
+ notifier()
+ return fn.apply(self)
+ },
+ {},
+ ))
+ markReactive(cached)
+ cached[$internalMemoFnMeta] = {
+ originalArgsLength,
+ } satisfies MemoFnMeta
+ return cached
},
})
} else {
@@ -165,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
new file mode 100644
index 0000000000..f2de587be1
--- /dev/null
+++ b/packages/angular-table/tests/angularReactivityFeature.test.ts
@@ -0,0 +1,237 @@
+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).concat(
+ Object.getOwnPropertyNames(Object.getPrototypeOf(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).concat(
+ Object.getOwnPropertyNames(Object.getPrototypeOf(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 and cells property reactivity', () => {
+ const flatRows = table.getRowModel().flatRows
+ flatRows.forEach((row, index) => {
+ const rowsPropertyKeys = Object.keys(row).concat(
+ Object.getOwnPropertyNames(Object.getPrototypeOf(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).concat(
+ Object.getOwnPropertyNames(Object.getPrototypeOf(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/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(
diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts
index abc4995b34..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,20 +14,17 @@ import {
stockFeatures,
} from '@tanstack/table-core'
import { RowModel, injectTable } from '../src'
-import {
- 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(() => ({
@@ -31,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', () => {
@@ -55,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', () => {
@@ -66,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)
})
@@ -122,152 +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('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/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..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'
@@ -53,10 +54,34 @@ export async function flushQueue() {
}
const staticComputedProperties = ['get', 'state']
+const staticNonComputedProperties = [
+ 'getIsSomeRowsPinned',
+ 'getColumn',
+ '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
+ }
+
if (staticComputedProperties.some((prop) => propertyName === prop)) {
return true
}
@@ -67,7 +92,12 @@ 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
}
+
+export function getFnReactiveCache(fn: any): any {
+ return fn._reactiveCache
+}
diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts
index 5734ec3e8a..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
@@ -82,7 +99,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 +120,10 @@ export const memo = , TDepArgs, TResult>({
return result
}
+
+ setMemoFnMeta(memoizedFn, { originalArgsLength: fn.length })
+
+ return memoizedFn
}
interface TableMemoOptions<
@@ -356,9 +377,8 @@ export function assignPrototypeAPIs<
return fn(this, ...args)
}
}
- Object.defineProperties(prototype[fnKey], {
- originalArgsLength: { value: fn.length },
- })
+
+ setMemoFnMeta(prototype[fnKey], { originalArgsLength: fn.length })
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0721ca44a1..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':
@@ -390,6 +445,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':
@@ -3439,10 +3549,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
| | | |