diff --git a/docs/config.json b/docs/config.json index 612e077867..1376674224 100644 --- a/docs/config.json +++ b/docs/config.json @@ -936,8 +936,11 @@ { "label": "angular", "children": [ - { "label": "Basic (Inject Table)", "to": "framework/angular/examples/basic-inject-table" }, - { "label": "Basic (App Table)", "to": "framework/angular/examples/basic-app-table" } + { "label": "Basic (injectTable)", "to": "framework/angular/examples/basic-inject-table" }, + { "label": "Basic (createAppTable)", "to": "framework/angular/examples/basic-app-table" }, + { "label": "Basic (External State)", "to": "framework/angular/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/angular/examples/basic-external-atoms" }, + { "label": "Header Groups", "to": "framework/angular/examples/column-groups" } ] }, { @@ -1020,15 +1023,22 @@ "label": "angular", "children": [ { "label": "Column Filters", "to": "framework/angular/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/angular/examples/filters-faceted" }, + { "label": "Fuzzy Search Filters", "to": "framework/angular/examples/filters-fuzzy" }, { "label": "Column Ordering", "to": "framework/angular/examples/column-ordering" }, { "label": "Column Pinning", "to": "framework/angular/examples/column-pinning" }, - { "label": "Column Pinning (Sticky)", "to": "framework/angular/examples/column-pinning-sticky" }, - { "label": "Column Resizing", "to": "framework/angular/examples/column-resizing-performant" }, + { "label": "Column Pinning (Split)", "to": "framework/angular/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/angular/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/angular/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/angular/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/angular/examples/column-resizing-performant" }, { "label": "Column Visibility", "to": "framework/angular/examples/column-visibility" }, - { "label": "Row Expanding", "to": "framework/angular/examples/expanding" }, - { "label": "Column Grouping", "to": "framework/angular/examples/grouping" }, + { "label": "Expanding", "to": "framework/angular/examples/expanding" }, + { "label": "Grouping", "to": "framework/angular/examples/grouping" }, + { "label": "Pagination", "to": "framework/angular/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/angular/examples/row-pinning" }, { "label": "Row Selection", "to": "framework/angular/examples/row-selection" }, - { "label": "Row Selection (Signal)", "to": "framework/angular/examples/row-selection-signal" }, + { "label": "Sorting", "to": "framework/angular/examples/sorting" }, { "label": "Kitchen Sink (All Features)", "to": "framework/angular/examples/kitchen-sink" } ] }, @@ -1192,8 +1202,14 @@ { "label": "Composable Tables", "to": "framework/angular/examples/composable-tables" }, { "label": "Custom Plugin", "to": "framework/angular/examples/custom-plugin" }, { "label": "Sub Components", "to": "framework/angular/examples/sub-components" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/angular/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/angular/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/angular/examples/virtualized-infinite-scrolling" }, + { "label": "With TanStack Form", "to": "framework/angular/examples/with-tanstack-form" }, + { "label": "With TanStack Query", "to": "framework/angular/examples/with-tanstack-query" }, { "label": "Fetch API data (SPA / SSR)", "to": "framework/angular/examples/remote-data" }, { "label": "Signal Input", "to": "framework/angular/examples/signal-input" }, + { "label": "Row Selection (Signal)", "to": "framework/angular/examples/row-selection-signal" }, { "label": "Editable data", "to": "framework/angular/examples/editable" }, { "label": "Row Drag & Drop", "to": "framework/angular/examples/row-dnd" } ] diff --git a/docs/framework/angular/guide/migrating.md b/docs/framework/angular/guide/migrating.md index 5cfc538939..0ee3ebaa3e 100644 --- a/docs/framework/angular/guide/migrating.md +++ b/docs/framework/angular/guide/migrating.md @@ -277,7 +277,9 @@ const v9Table = injectTable(() => ({ ### Accessing State -In v8, you accessed state via `table.getState()`. In v9, state is accessed via the store: +In v8, you accessed state via `table.getState()`. In v9, read the specific +state slice from `table.atoms..get()` where possible. Use `table.state` +when you need the full flat state shape, such as debug JSON. ```ts // v8 @@ -285,9 +287,13 @@ const state = table.getState() const v8 = table.getState() const { sorting, pagination } = v8 -// v9 - via the store -const fullState = table.store.state -const v9 = table.store.state +// v9 - per-slice reads, preferred for Angular render code +const sorting = table.atoms.sorting.get() +const pagination = table.atoms.pagination.get() + +// v9 - full-state flat proxy +const fullState = table.state +const v9 = table.state const { sorting: v9Sorting, pagination: v9Pagination } = v9 ``` @@ -344,7 +350,7 @@ class TableCmp { // Provide an equality function for object slices readonly pagination = computed( - () => this.table.store.state.pagination, + () => this.table.atoms.pagination.get(), { equal: shallow }, ) @@ -732,7 +738,7 @@ The `RowData` type is now more restrictive. - [ ] Migrate `get*RowModel()` options to `_rowModels` - [ ] Update row model factories to include `Fns` parameters where needed - [ ] Update TypeScript types to include `TFeatures` generic -- [ ] Update state access: `table.getState()` → `table.store.state` +- [ ] Update state access: `table.getState().slice` → `table.atoms..get()` where possible; use `table.state` for full-state/debug reads - [ ] Update `createColumnHelper()` → `createColumnHelper()` - [ ] Replace `enablePinning` with `enableColumnPinning`/`enableRowPinning` if used - [ ] Rename `sortingFn` → `sortFn` in column definitions @@ -747,7 +753,7 @@ The `RowData` type is now more restrictive. ## Examples Check out these examples to see v9 patterns in action: -- [Basic](../examples/basic) +- [Basic (Inject Table)](../examples/basic-inject-table) - [Basic (App Table)](../examples/basic-app-table) - [Filters](../examples/filters) - [Column Ordering](../examples/column-ordering) diff --git a/docs/framework/angular/guide/table-state.md b/docs/framework/angular/guide/table-state.md index 1f78ebffa9..ad6ed33096 100644 --- a/docs/framework/angular/guide/table-state.md +++ b/docs/framework/angular/guide/table-state.md @@ -28,7 +28,8 @@ A table instance has a few state surfaces: - `table.baseAtoms` are the internal writable atoms created from the resolved initial state. - `table.atoms` are readonly derived atoms exposed per registered state slice. -- `table.store` is a readonly flat TanStack Store derived by putting all of the registered `table.atoms` together. +- `table.state` is a readonly flat proxy over the registered `table.atoms`, useful for full-state debug output. +- `table.store` is the underlying readonly flat TanStack Store. Prefer `table.atoms` or `table.state` in app code. The Angular adapter provides `angularReactivity(injector)` as the table's reactivity binding. Core readonly atoms are Angular `computed` values, writable atoms are Angular `signal` values, and subscriptions bridge through `toObservable(computed(...), { injector })`. `injectTable` reruns the options initializer when Angular signals read inside it change, then calls `table.setOptions`. @@ -80,11 +81,11 @@ const pagination = this.table.atoms.pagination.get() const sorting = this.table.atoms.sorting.get() ``` -You can also read the current flat store snapshot: +Use `table.state` when you need the current flat state shape, such as debug JSON: ```ts -const tableState = this.table.store.state -const pagination = this.table.store.state.pagination +const tableState = this.table.state +const stateJson = JSON.stringify(this.table.state, null, 2) ``` Atom reads are signal reads in Angular. If `this.table.atoms.pagination.get()` is used in a template expression, `computed(...)`, or `effect(...)`, Angular tracks it and updates when that atom changes. @@ -115,11 +116,11 @@ readonly pagination = computed( readonly pageIndex = computed(() => this.pagination().pageIndex) ``` -You can also select from the flat store if that is more convenient. +You can also select from the flat state proxy if that is more convenient, but prefer direct atoms for narrow render reads. ```ts readonly pagination = computed( - () => this.table.store.state.pagination, + () => this.table.state.pagination, { equal: shallow }, ) ``` diff --git a/docs/framework/angular/reference/functions/injectTable.md b/docs/framework/angular/reference/functions/injectTable.md index 0ff8f15357..1ce3c1d731 100644 --- a/docs/framework/angular/reference/functions/injectTable.md +++ b/docs/framework/angular/reference/functions/injectTable.md @@ -9,7 +9,7 @@ title: injectTable function injectTable(options): AngularTable; ``` -Defined in: [packages/angular-table/src/injectTable.ts:92](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L92) +Defined in: [packages/angular-table/src/injectTable.ts:106](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L106) Creates and returns an Angular-reactive table instance. diff --git a/docs/framework/angular/reference/type-aliases/AngularTable.md b/docs/framework/angular/reference/type-aliases/AngularTable.md index 41e91ea224..ead0a2d42d 100644 --- a/docs/framework/angular/reference/type-aliases/AngularTable.md +++ b/docs/framework/angular/reference/type-aliases/AngularTable.md @@ -6,10 +6,34 @@ title: AngularTable # Type Alias: AngularTable\ ```ts -type AngularTable = Table; +type AngularTable = Table & object; ``` -Defined in: [packages/angular-table/src/injectTable.ts:31](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L31) +Defined in: [packages/angular-table/src/injectTable.ts:33](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L33) + +## Type Declaration + +### state + +```ts +readonly state: Readonly>; +``` + +The current table state exposed as a flat proxy. Prefer +`table.atoms..get()` when reading a specific slice. + +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.atoms..get()` for template/render reads +of a specific state slice, `table.state` for full-state debug snapshots, or +Angular computed values around explicit selectors. `table.store.state` is a +current-value snapshot and is easy to misuse in render code. ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/SubscribeSource.md b/docs/framework/angular/reference/type-aliases/SubscribeSource.md index 7b27774494..a384d0e002 100644 --- a/docs/framework/angular/reference/type-aliases/SubscribeSource.md +++ b/docs/framework/angular/reference/type-aliases/SubscribeSource.md @@ -13,7 +13,7 @@ type SubscribeSource = | ReadonlyStore; ``` -Defined in: [packages/angular-table/src/injectTable.ts:25](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L25) +Defined in: [packages/angular-table/src/injectTable.ts:27](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L27) ## Type Parameters diff --git a/docs/framework/lit/reference/classes/TableController.md b/docs/framework/lit/reference/classes/TableController.md index 45af560b02..044280e1aa 100644 --- a/docs/framework/lit/reference/classes/TableController.md +++ b/docs/framework/lit/reference/classes/TableController.md @@ -5,7 +5,7 @@ title: TableController # Class: TableController\ -Defined in: [TableController.ts:132](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L132) +Defined in: [TableController.ts:139](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L139) A Lit ReactiveController for TanStack Table integration. @@ -57,7 +57,7 @@ class MyTable extends LitElement { new TableController(host): TableController; ``` -Defined in: [TableController.ts:143](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L143) +Defined in: [TableController.ts:150](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L150) #### Parameters @@ -77,7 +77,7 @@ Defined in: [TableController.ts:143](https://github.com/TanStack/table/blob/main host: ReactiveControllerHost; ``` -Defined in: [TableController.ts:136](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L136) +Defined in: [TableController.ts:143](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L143) ## Methods @@ -87,7 +87,7 @@ Defined in: [TableController.ts:136](https://github.com/TanStack/table/blob/main hostConnected(): void; ``` -Defined in: [TableController.ts:244](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L244) +Defined in: [TableController.ts:251](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L251) Called when the host is connected to the component tree. For custom element hosts, this corresponds to the `connectedCallback()` lifecycle, @@ -111,7 +111,7 @@ ReactiveController.hostConnected hostDisconnected(): void; ``` -Defined in: [TableController.ts:248](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L248) +Defined in: [TableController.ts:255](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L255) Called when the host is disconnected from the component tree. For custom element hosts, this corresponds to the `disconnectedCallback()` lifecycle, @@ -136,7 +136,7 @@ ReactiveController.hostDisconnected table(tableOptions, selector?): LitTable; ``` -Defined in: [TableController.ts:163](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L163) +Defined in: [TableController.ts:170](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L170) Returns the Lit-backed table instance for the current render pass. diff --git a/docs/framework/lit/reference/type-aliases/LitTable.md b/docs/framework/lit/reference/type-aliases/LitTable.md index 6aa35c54e7..efc37cd0d6 100644 --- a/docs/framework/lit/reference/type-aliases/LitTable.md +++ b/docs/framework/lit/reference/type-aliases/LitTable.md @@ -6,7 +6,7 @@ title: LitTable # Type Alias: LitTable\ ```ts -type LitTable = Table & object; +type LitTable = Omit, "store"> & object; ``` Defined in: [TableController.ts:30](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L30) @@ -41,7 +41,7 @@ readonly state: Readonly; ``` The selected state of the table. This state may not match the structure of -`table.store.state` because it is selected by the `selector` function that +the full table state because it is selected by the selector function that you pass as the 2nd argument to `controller.table()`. #### Example @@ -54,6 +54,19 @@ const table = this.tableController.table(options, (state) => ({ console.log(table.state.globalFilter) ``` +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state` for render reads, +`table.atoms..get()` for slice snapshots, or `table.Subscribe` for +explicit subscriptions. `table.store.state` is a current-value snapshot and +is easy to misuse in render code. + ### Subscribe() ```ts diff --git a/docs/framework/preact/guide/create-table-hook.md b/docs/framework/preact/guide/create-table-hook.md index 138e749bc0..c709468e3e 100644 --- a/docs/framework/preact/guide/create-table-hook.md +++ b/docs/framework/preact/guide/create-table-hook.md @@ -2,7 +2,7 @@ title: createTableHook Guide --- -`createTableHook` is an advanced API for building reusable, composable table configurations in Preact. It mirrors the [React `createTableHook` API](../react/guide/create-table-hook) — you define features, row models, and pre-bound components once, then reuse them across multiple tables with minimal boilerplate. +`createTableHook` is an advanced API for building reusable, composable table configurations in Preact. It mirrors the [React `createTableHook` API](../../react/guide/create-table-hook) — you define features, row models, and pre-bound components once, then reuse them across multiple tables with minimal boilerplate. > **When to use it:** Use `createTableHook` when you have multiple tables that share the same configuration. For a single table, `useTable` is sufficient. @@ -165,5 +165,5 @@ Same as React: `table.AppTable`, `table.AppHeader`, `table.AppCell`, and `table. ## See Also -- [React createTableHook Guide](../react/guide/create-table-hook) — The React guide has more detailed examples and the same API. -- [Composable Tables (React)](../react/examples/composable-tables) — Reference implementation (Preact API mirrors React). +- [React createTableHook Guide](../../react/guide/create-table-hook) — The React guide has more detailed examples and the same API. +- [Composable Tables (React)](../../react/examples/composable-tables) — Reference implementation (Preact API mirrors React). diff --git a/docs/framework/preact/reference/functions/useTable.md b/docs/framework/preact/reference/functions/useTable.md index 59ccf3e834..fa08509d53 100644 --- a/docs/framework/preact/reference/functions/useTable.md +++ b/docs/framework/preact/reference/functions/useTable.md @@ -9,7 +9,7 @@ title: useTable function useTable(tableOptions, selector?): PreactTable; ``` -Defined in: [useTable.ts:103](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L103) +Defined in: [useTable.ts:113](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L113) Creates a Preact table instance backed by TanStack Store atoms. diff --git a/docs/framework/preact/reference/type-aliases/PreactTable.md b/docs/framework/preact/reference/type-aliases/PreactTable.md index 99c7f20b1f..a3c552c4ca 100644 --- a/docs/framework/preact/reference/type-aliases/PreactTable.md +++ b/docs/framework/preact/reference/type-aliases/PreactTable.md @@ -6,7 +6,7 @@ title: PreactTable # Type Alias: PreactTable\ ```ts -type PreactTable = Table & object; +type PreactTable = Omit, "store"> & object; ``` Defined in: [useTable.ts:19](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L19) @@ -44,7 +44,23 @@ Use this utility component instead of manually calling flexRender. readonly state: Readonly; ``` -The selected state of the table. This state may not match the structure of `table.store.state` because it is selected by the `selector` function that you pass as the 2nd argument to `useTable`. +The selected state of the table. This state may not match the structure of +the full table state because it is selected by the selector function that +you pass as the 2nd argument to `useTable`. + +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state` for render reads, +`table.atoms..get()` for slice snapshots, or +`table.Subscribe` / `useSelector(table.store, selector)` for explicit +subscriptions. `table.store.state` is a current-value snapshot and is easy +to misuse in render code. ### Subscribe() diff --git a/docs/framework/react/reference/index/functions/useTable.md b/docs/framework/react/reference/index/functions/useTable.md index 439966c38b..9ccbbd6356 100644 --- a/docs/framework/react/reference/index/functions/useTable.md +++ b/docs/framework/react/reference/index/functions/useTable.md @@ -9,7 +9,7 @@ title: useTable function useTable(tableOptions, selector?): ReactTable; ``` -Defined in: [useTable.ts:132](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L132) +Defined in: [useTable.ts:142](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L142) Creates a React table instance backed by TanStack Store atoms. diff --git a/docs/framework/react/reference/index/type-aliases/ReactTable.md b/docs/framework/react/reference/index/type-aliases/ReactTable.md index ecf261b102..ba687dfb15 100644 --- a/docs/framework/react/reference/index/type-aliases/ReactTable.md +++ b/docs/framework/react/reference/index/type-aliases/ReactTable.md @@ -6,7 +6,7 @@ title: ReactTable # Type Alias: ReactTable\ ```ts -type ReactTable = Table & object; +type ReactTable = Omit, "store"> & object; ``` Defined in: [useTable.ts:21](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L21) @@ -59,7 +59,9 @@ flexRender(footer.column.columnDef.footer, footer.getContext()) readonly state: Readonly; ``` -The selected state of the table. This state may not match the structure of `table.store.state` because it is selected by the `selector` function that you pass as the 2nd argument to `useTable`. +The selected state of the table. This state may not match the structure of +the full table state because it is selected by the selector function that +you pass as the 2nd argument to `useTable`. #### Example @@ -69,6 +71,20 @@ const table = useTable(options, (state) => ({ globalFilter: state.globalFilter } console.log(table.state.globalFilter) ``` +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state` for render reads, +`table.atoms..get()` for slice snapshots, or +`table.Subscribe` / `useSelector(table.store, selector)` for explicit +subscriptions. `table.store.state` is a current-value snapshot and is easy +to misuse in render code. + ### Subscribe() ```ts diff --git a/docs/framework/react/reference/legacy/type-aliases/LegacyReactTable.md b/docs/framework/react/reference/legacy/type-aliases/LegacyReactTable.md index 2175cf834d..c88d558ec7 100644 --- a/docs/framework/react/reference/legacy/type-aliases/LegacyReactTable.md +++ b/docs/framework/react/reference/legacy/type-aliases/LegacyReactTable.md @@ -29,7 +29,7 @@ Returns the current table state. #### Deprecated -In v9, access state directly via `table.state` or use `table.store.state` for the full state. +In v9, access state directly via `table.state` or use `table.state` for the full state. ### ~~setState()~~ diff --git a/docs/framework/solid/reference/functions/createTable.md b/docs/framework/solid/reference/functions/createTable.md index 02a6d3fd5a..f74d5d1419 100644 --- a/docs/framework/solid/reference/functions/createTable.md +++ b/docs/framework/solid/reference/functions/createTable.md @@ -9,7 +9,7 @@ title: createTable function createTable(tableOptions, selector?): SolidTable; ``` -Defined in: [createTable.ts:99](https://github.com/TanStack/table/blob/main/packages/solid-table/src/createTable.ts#L99) +Defined in: [createTable.ts:109](https://github.com/TanStack/table/blob/main/packages/solid-table/src/createTable.ts#L109) Creates a Solid table instance backed by Solid-aware TanStack Store atoms. diff --git a/docs/framework/solid/reference/type-aliases/SolidTable.md b/docs/framework/solid/reference/type-aliases/SolidTable.md index 4465dc6993..37c72d0b6d 100644 --- a/docs/framework/solid/reference/type-aliases/SolidTable.md +++ b/docs/framework/solid/reference/type-aliases/SolidTable.md @@ -6,7 +6,7 @@ title: SolidTable # Type Alias: SolidTable\ ```ts -type SolidTable = Table & object; +type SolidTable = Omit, "store"> & object; ``` Defined in: [createTable.ts:27](https://github.com/TanStack/table/blob/main/packages/solid-table/src/createTable.ts#L27) @@ -37,7 +37,9 @@ rendering headers, cells, or footers with custom markup. Mirrors the readonly state: Accessor>; ``` -The selected state of the table. This state may not match the structure of `table.store.state` because it is selected by the `selector` function that you pass as the 2nd argument to `createTable`. +The selected state of the table. This state may not match the structure of +the full table state because it is selected by the selector function that +you pass as the 2nd argument to `createTable`. #### Example @@ -47,6 +49,20 @@ const table = createTable(options, (state) => ({ globalFilter: state.globalFilte console.log(table.state().globalFilter) ``` +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state()` for component-level reactive reads, +`table.atoms..get()` for slice-level reactive reads, or +`table.Subscribe` / `useSelector(table.store, selector)` for explicit +subscriptions. Reading `table.state` directly does not follow Solid's +accessor convention and may not update render code as expected. + ### Subscribe() ```ts diff --git a/docs/framework/svelte/reference/functions/createTable.md b/docs/framework/svelte/reference/functions/createTable.md index f608bd0969..e07b5ac688 100644 --- a/docs/framework/svelte/reference/functions/createTable.md +++ b/docs/framework/svelte/reference/functions/createTable.md @@ -9,7 +9,7 @@ title: createTable function createTable(tableOptions, selector?): SvelteTable; ``` -Defined in: [packages/svelte-table/src/createTable.svelte.ts:55](https://github.com/TanStack/table/blob/main/packages/svelte-table/src/createTable.svelte.ts#L55) +Defined in: [packages/svelte-table/src/createTable.svelte.ts:65](https://github.com/TanStack/table/blob/main/packages/svelte-table/src/createTable.svelte.ts#L65) Creates a Svelte 5 table instance backed by rune-aware TanStack Store atoms. diff --git a/docs/framework/svelte/reference/type-aliases/FlexRender.md b/docs/framework/svelte/reference/type-aliases/FlexRender.md index f269256a43..4ea2097953 100644 --- a/docs/framework/svelte/reference/type-aliases/FlexRender.md +++ b/docs/framework/svelte/reference/type-aliases/FlexRender.md @@ -9,4 +9,4 @@ title: FlexRender type FlexRender = SvelteComponent; ``` -Defined in: node\_modules/.pnpm/svelte@5.55.5\_@typescript-eslint+types@8.59.3/node\_modules/svelte/types/index.d.ts:3204 +Defined in: node\_modules/.pnpm/svelte@5.55.9\_@typescript-eslint+types@8.59.4/node\_modules/svelte/types/index.d.ts:3204 diff --git a/docs/framework/svelte/reference/type-aliases/SvelteTable.md b/docs/framework/svelte/reference/type-aliases/SvelteTable.md index 682bcc2a4f..0ae4dea06c 100644 --- a/docs/framework/svelte/reference/type-aliases/SvelteTable.md +++ b/docs/framework/svelte/reference/type-aliases/SvelteTable.md @@ -6,7 +6,7 @@ title: SvelteTable # Type Alias: SvelteTable\ ```ts -type SvelteTable = Table & object; +type SvelteTable = Omit, "store"> & object; ``` Defined in: [packages/svelte-table/src/createTable.svelte.ts:14](https://github.com/TanStack/table/blob/main/packages/svelte-table/src/createTable.svelte.ts#L14) @@ -19,7 +19,9 @@ Defined in: [packages/svelte-table/src/createTable.svelte.ts:14](https://github. readonly state: Readonly; ``` -The selected state of the table. This state may not match the structure of `table.store.state` because it is selected by the `selector` function that you pass as the 2nd argument to `createTable`. +The selected state of the table. This state may not match the structure of +the full table state because it is selected by the selector function that +you pass as the 2nd argument to `createTable`. #### Example @@ -29,6 +31,20 @@ const table = createTable(options, (state) => ({ globalFilter: state.globalFilte console.log(table.state.globalFilter) ``` +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state` for render reads, +`table.atoms..get()` for slice snapshots, or +`useSelector(table.store, selector)` for explicit subscriptions. +`table.store.state` is a current-value snapshot and is easy to misuse in +render code. + ## Type Parameters ### TFeatures diff --git a/docs/framework/svelte/reference/variables/FlexRender.md b/docs/framework/svelte/reference/variables/FlexRender.md index a22b57d50e..1e2fd7c29c 100644 --- a/docs/framework/svelte/reference/variables/FlexRender.md +++ b/docs/framework/svelte/reference/variables/FlexRender.md @@ -9,4 +9,4 @@ title: FlexRender const FlexRender: LegacyComponentType; ``` -Defined in: node\_modules/.pnpm/svelte@5.55.5\_@typescript-eslint+types@8.59.3/node\_modules/svelte/types/index.d.ts:3204 +Defined in: node\_modules/.pnpm/svelte@5.55.9\_@typescript-eslint+types@8.59.4/node\_modules/svelte/types/index.d.ts:3204 diff --git a/docs/framework/vue/reference/functions/useTable.md b/docs/framework/vue/reference/functions/useTable.md index 2c37444bf1..cd83466cad 100644 --- a/docs/framework/vue/reference/functions/useTable.md +++ b/docs/framework/vue/reference/functions/useTable.md @@ -9,7 +9,7 @@ title: useTable function useTable(tableOptions, selector?): VueTable; ``` -Defined in: [packages/vue-table/src/useTable.ts:130](https://github.com/TanStack/table/blob/main/packages/vue-table/src/useTable.ts#L130) +Defined in: [packages/vue-table/src/useTable.ts:140](https://github.com/TanStack/table/blob/main/packages/vue-table/src/useTable.ts#L140) Creates a Vue table instance backed by Vue-aware TanStack Store atoms. diff --git a/docs/framework/vue/reference/type-aliases/VueTable.md b/docs/framework/vue/reference/type-aliases/VueTable.md index f7ebbdc8a2..fb1d97a01d 100644 --- a/docs/framework/vue/reference/type-aliases/VueTable.md +++ b/docs/framework/vue/reference/type-aliases/VueTable.md @@ -6,7 +6,7 @@ title: VueTable # Type Alias: VueTable\ ```ts -type VueTable = Table & object; +type VueTable = Omit, "store"> & object; ``` Defined in: [packages/vue-table/src/useTable.ts:61](https://github.com/TanStack/table/blob/main/packages/vue-table/src/useTable.ts#L61) @@ -19,7 +19,9 @@ Defined in: [packages/vue-table/src/useTable.ts:61](https://github.com/TanStack/ readonly state: Readonly; ``` -The selected state of the table. This state may not match the structure of `table.store.state` because it is selected by the `selector` function that you pass as the 2nd argument to `useTable`. +The selected state of the table. This state may not match the structure of +the full table state because it is selected by the selector function that +you pass as the 2nd argument to `useTable`. #### Example @@ -29,6 +31,20 @@ const table = useTable(options, (state) => ({ globalFilter: state.globalFilter } console.log(table.state.globalFilter) ``` +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state` for render reads, +`table.atoms..get()` for slice snapshots, or +`table.Subscribe` / `useSelector(table.store, selector)` for explicit +subscriptions. `table.store.state` is a current-value snapshot and is easy +to misuse in render code. + ### Subscribe() ```ts diff --git a/docs/guide/column-faceting.md b/docs/guide/column-faceting.md index 883ee9008d..f86e5c5258 100644 --- a/docs/guide/column-faceting.md +++ b/docs/guide/column-faceting.md @@ -28,6 +28,10 @@ Want to skip to the implementation? Check out these examples: - [Faceted Filters](../framework/vue/examples/filters-faceted) +# Angular + +- [Faceted Filters](../framework/angular/examples/filters-faceted) + # Lit - [Faceted Filters](../framework/lit/examples/filters-faceted) diff --git a/docs/guide/column-filtering.md b/docs/guide/column-filtering.md index 1e1cde98fc..996ec907d3 100644 --- a/docs/guide/column-filtering.md +++ b/docs/guide/column-filtering.md @@ -41,6 +41,8 @@ Want to skip to the implementation? Check out these examples: # Angular - [Column Filters](../framework/angular/examples/filters) +- [Faceted Filters](../framework/angular/examples/filters-faceted) +- [Fuzzy Search](../framework/angular/examples/filters-fuzzy) # Lit diff --git a/docs/guide/column-pinning.md b/docs/guide/column-pinning.md index 664a525c1c..b2c9f01d68 100644 --- a/docs/guide/column-pinning.md +++ b/docs/guide/column-pinning.md @@ -41,6 +41,7 @@ Want to skip to the implementation? Check out these examples: # Angular - [Column Pinning](../framework/angular/examples/column-pinning) +- [Column Pinning Split](../framework/angular/examples/column-pinning-split) - [Sticky Column Pinning](../framework/angular/examples/column-pinning-sticky) # Lit diff --git a/docs/guide/column-resizing.md b/docs/guide/column-resizing.md index cf4b207ce4..aa7a791960 100644 --- a/docs/guide/column-resizing.md +++ b/docs/guide/column-resizing.md @@ -35,6 +35,7 @@ Want to skip to the implementation? Check out these examples: # Angular +- [Column Resizing](../framework/angular/examples/column-resizing) - [Performant Column Resizing](../framework/angular/examples/column-resizing-performant) # Lit diff --git a/docs/guide/column-sizing.md b/docs/guide/column-sizing.md index a5b0441787..4f8c8ed475 100644 --- a/docs/guide/column-sizing.md +++ b/docs/guide/column-sizing.md @@ -28,6 +28,10 @@ Want to skip to the implementation? Check out these examples: - [Column Sizing](../framework/vue/examples/column-sizing) +# Angular + +- [Column Sizing](../framework/angular/examples/column-sizing) + # Lit - [Column Sizing](../framework/lit/examples/column-sizing) diff --git a/docs/guide/fuzzy-filtering.md b/docs/guide/fuzzy-filtering.md index 8d7c09f68c..e4109cc44b 100644 --- a/docs/guide/fuzzy-filtering.md +++ b/docs/guide/fuzzy-filtering.md @@ -28,6 +28,10 @@ Want to skip to the implementation? Check out these examples: - [Fuzzy Search](../framework/vue/examples/filters-fuzzy) +# Angular + +- [Fuzzy Search](../framework/angular/examples/filters-fuzzy) + # Lit - [Fuzzy Search](../framework/lit/examples/filters-fuzzy) diff --git a/docs/guide/global-filtering.md b/docs/guide/global-filtering.md index 26c1708031..73eae139f4 100644 --- a/docs/guide/global-filtering.md +++ b/docs/guide/global-filtering.md @@ -41,6 +41,8 @@ Want to skip to the implementation? Check out these examples: # Angular - [Column Filters](../framework/angular/examples/filters) +- [Faceted Filters](../framework/angular/examples/filters-faceted) +- [Fuzzy Search](../framework/angular/examples/filters-fuzzy) # Lit diff --git a/docs/guide/pagination.md b/docs/guide/pagination.md index 2edba0ab4b..67732baac3 100644 --- a/docs/guide/pagination.md +++ b/docs/guide/pagination.md @@ -35,6 +35,8 @@ Want to skip to the implementation? Check out these examples: # Angular +- [Pagination](../framework/angular/examples/pagination) +- [With TanStack Query](../framework/angular/examples/with-tanstack-query) - [Remote Data](../framework/angular/examples/remote-data) # Lit diff --git a/docs/guide/row-pinning.md b/docs/guide/row-pinning.md index 4a8f098def..b5e3fa3c6e 100644 --- a/docs/guide/row-pinning.md +++ b/docs/guide/row-pinning.md @@ -28,6 +28,10 @@ Want to skip to the implementation? Check out these examples: - [Row Pinning](../framework/vue/examples/row-pinning) +# Angular + +- [Row Pinning](../framework/angular/examples/row-pinning) + # Lit - [Row Pinning](../framework/lit/examples/row-pinning) diff --git a/docs/guide/sorting.md b/docs/guide/sorting.md index ee652c94b6..11e91a7572 100644 --- a/docs/guide/sorting.md +++ b/docs/guide/sorting.md @@ -28,6 +28,10 @@ Want to skip to the implementation? Check out these examples: - [Sorting](../framework/vue/examples/sorting) +# Angular + +- [Sorting](../framework/angular/examples/sorting) + # Lit - [Sorting](../framework/lit/examples/sorting) diff --git a/docs/guide/virtualization.md b/docs/guide/virtualization.md index b2f08fba8b..381a859585 100644 --- a/docs/guide/virtualization.md +++ b/docs/guide/virtualization.md @@ -32,6 +32,12 @@ Want to skip to the implementation? Check out these examples: - [Virtualized Rows](../framework/vue/examples/virtualized-rows) - [Virtualized Infinite Scrolling](../framework/vue/examples/virtualized-infinite-scrolling) +# Angular + +- [Virtualized Columns](../framework/angular/examples/virtualized-columns) +- [Virtualized Rows](../framework/angular/examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../framework/angular/examples/virtualized-infinite-scrolling) + # Lit - [Virtualized Columns](../framework/lit/examples/virtualized-columns) diff --git a/docs/reference/index/functions/assignPrototypeAPIs.md b/docs/reference/index/functions/assignPrototypeAPIs.md index 13377f6ad9..e36969115a 100644 --- a/docs/reference/index/functions/assignPrototypeAPIs.md +++ b/docs/reference/index/functions/assignPrototypeAPIs.md @@ -13,7 +13,7 @@ function assignPrototypeAPIs( apis): void; ``` -Defined in: [utils.ts:378](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L378) +Defined in: [utils.ts:382](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L382) Assigns API methods to a prototype object for memory-efficient method sharing. All instances created with this prototype will share the same method references. diff --git a/docs/reference/index/functions/assignTableAPIs.md b/docs/reference/index/functions/assignTableAPIs.md index e57405c7e2..d595049d95 100644 --- a/docs/reference/index/functions/assignTableAPIs.md +++ b/docs/reference/index/functions/assignTableAPIs.md @@ -12,7 +12,7 @@ function assignTableAPIs( apis): void; ``` -Defined in: [utils.ts:336](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L336) +Defined in: [utils.ts:340](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L340) Assigns Table API methods directly to the table instance. Unlike row/cell/column/header, the table is a singleton so methods are assigned directly. diff --git a/docs/reference/index/functions/callMemoOrStaticFn.md b/docs/reference/index/functions/callMemoOrStaticFn.md index c66df8423c..8bdc12f8f5 100644 --- a/docs/reference/index/functions/callMemoOrStaticFn.md +++ b/docs/reference/index/functions/callMemoOrStaticFn.md @@ -13,7 +13,7 @@ function callMemoOrStaticFn( args): ReturnType; ``` -Defined in: [utils.ts:424](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L424) +Defined in: [utils.ts:428](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L428) Looks to run the memoized function with the builder pattern on the object if it exists, otherwise fallback to the static method passed in. diff --git a/docs/reference/index/functions/getFunctionNameInfo.md b/docs/reference/index/functions/getFunctionNameInfo.md index f0c914d94b..4119b7b85d 100644 --- a/docs/reference/index/functions/getFunctionNameInfo.md +++ b/docs/reference/index/functions/getFunctionNameInfo.md @@ -9,7 +9,7 @@ title: getFunctionNameInfo function getFunctionNameInfo(staticFnName, splitBy): object; ``` -Defined in: [utils.ts:319](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L319) +Defined in: [utils.ts:323](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L323) Assumes that a function name is in the format of `parentName_fnKey` and returns the `fnKey` and `fnName` in the format of `parentName.fnKey`. diff --git a/docs/reference/index/interfaces/API.md b/docs/reference/index/interfaces/API.md index 731d88b86c..35c98084f7 100644 --- a/docs/reference/index/interfaces/API.md +++ b/docs/reference/index/interfaces/API.md @@ -5,7 +5,7 @@ title: API # Interface: API\ -Defined in: [utils.ts:306](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L306) +Defined in: [utils.ts:310](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L310) ## Type Parameters @@ -25,7 +25,7 @@ Defined in: [utils.ts:306](https://github.com/TanStack/table/blob/main/packages/ fn: (...args) => any; ``` -Defined in: [utils.ts:307](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L307) +Defined in: [utils.ts:311](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L311) #### Parameters @@ -45,7 +45,7 @@ Defined in: [utils.ts:307](https://github.com/TanStack/table/blob/main/packages/ optional memoDeps: (depArgs?) => any[] | undefined; ``` -Defined in: [utils.ts:308](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L308) +Defined in: [utils.ts:312](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L312) #### Parameters diff --git a/docs/reference/index/interfaces/PrototypeAPI.md b/docs/reference/index/interfaces/PrototypeAPI.md index 05f7ab603d..35b6caeab1 100644 --- a/docs/reference/index/interfaces/PrototypeAPI.md +++ b/docs/reference/index/interfaces/PrototypeAPI.md @@ -5,7 +5,7 @@ title: PrototypeAPI # Interface: PrototypeAPI\ -Defined in: [utils.ts:361](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L361) +Defined in: [utils.ts:365](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L365) ## Type Parameters @@ -25,7 +25,7 @@ Defined in: [utils.ts:361](https://github.com/TanStack/table/blob/main/packages/ fn: (self, ...args) => any; ``` -Defined in: [utils.ts:362](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L362) +Defined in: [utils.ts:366](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L366) #### Parameters @@ -49,7 +49,7 @@ Defined in: [utils.ts:362](https://github.com/TanStack/table/blob/main/packages/ optional memoDeps: (self, depArgs?) => any[] | undefined; ``` -Defined in: [utils.ts:363](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L363) +Defined in: [utils.ts:367](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L367) #### Parameters diff --git a/docs/reference/index/interfaces/TableOptions_Core.md b/docs/reference/index/interfaces/TableOptions_Core.md index 8aff92be1e..97000adc82 100644 --- a/docs/reference/index/interfaces/TableOptions_Core.md +++ b/docs/reference/index/interfaces/TableOptions_Core.md @@ -228,7 +228,7 @@ getSubRows: row => row.subRows readonly optional initialState: Partial>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:102](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L102) +Defined in: [core/table/coreTablesFeature.types.ts:109](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L109) Optionally provide starting values for registered table state slices. Feature reset APIs use this value by default, and many reset APIs accept @@ -241,13 +241,32 @@ object later does not reset table state, so it does not need to be stable. *** +### key? + +```ts +readonly optional key: string; +``` + +Defined in: [core/table/coreTablesFeature.types.ts:102](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L102) + +Optional key used to identify this table instance. + +This is used by TanStack Table Devtools to register and select tables. It is +not required unless the table is passed to devtools. + +#### Inherited from + +[`TableOptions_Table`](TableOptions_Table.md).[`key`](TableOptions_Table.md#key) + +*** + ### mergeOptions()? ```ts readonly optional mergeOptions: (defaultOptions, options) => TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:106](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L106) +Defined in: [core/table/coreTablesFeature.types.ts:113](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L113) This option is used to optionally implement the merging of table options. @@ -277,7 +296,7 @@ This option is used to optionally implement the merging of table options. readonly optional meta: TableMeta; ``` -Defined in: [core/table/coreTablesFeature.types.ts:113](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L113) +Defined in: [core/table/coreTablesFeature.types.ts:120](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L120) You can pass any object to `options.meta` and access it anywhere the `table` is available via `table.options.meta`. @@ -309,7 +328,7 @@ Value used when the desired value is not found in the data. readonly optional state: Partial>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:121](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L121) +Defined in: [core/table/coreTablesFeature.types.ts:128](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L128) Optionally provide externally managed values for individual state slices. diff --git a/docs/reference/index/interfaces/TableOptions_Table.md b/docs/reference/index/interfaces/TableOptions_Table.md index da50fa6a91..dfdf66ff35 100644 --- a/docs/reference/index/interfaces/TableOptions_Table.md +++ b/docs/reference/index/interfaces/TableOptions_Table.md @@ -97,7 +97,7 @@ The data for the table to display. When the `data` option changes reference, the readonly optional initialState: Partial>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:102](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L102) +Defined in: [core/table/coreTablesFeature.types.ts:109](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L109) Optionally provide starting values for registered table state slices. Feature reset APIs use this value by default, and many reset APIs accept @@ -106,13 +106,28 @@ object later does not reset table state, so it does not need to be stable. *** +### key? + +```ts +readonly optional key: string; +``` + +Defined in: [core/table/coreTablesFeature.types.ts:102](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L102) + +Optional key used to identify this table instance. + +This is used by TanStack Table Devtools to register and select tables. It is +not required unless the table is passed to devtools. + +*** + ### mergeOptions()? ```ts readonly optional mergeOptions: (defaultOptions, options) => TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:106](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L106) +Defined in: [core/table/coreTablesFeature.types.ts:113](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L113) This option is used to optionally implement the merging of table options. @@ -138,7 +153,7 @@ This option is used to optionally implement the merging of table options. readonly optional meta: TableMeta; ``` -Defined in: [core/table/coreTablesFeature.types.ts:113](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L113) +Defined in: [core/table/coreTablesFeature.types.ts:120](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L120) You can pass any object to `options.meta` and access it anywhere the `table` is available via `table.options.meta`. @@ -150,7 +165,7 @@ You can pass any object to `options.meta` and access it anywhere the `table` is readonly optional state: Partial>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:121](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L121) +Defined in: [core/table/coreTablesFeature.types.ts:128](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L128) Optionally provide externally managed values for individual state slices. diff --git a/docs/reference/index/interfaces/Table_CoreProperties.md b/docs/reference/index/interfaces/Table_CoreProperties.md index 7c609f1a27..79acb59754 100644 --- a/docs/reference/index/interfaces/Table_CoreProperties.md +++ b/docs/reference/index/interfaces/Table_CoreProperties.md @@ -5,7 +5,7 @@ title: Table_CoreProperties # Interface: Table\_CoreProperties\ -Defined in: [core/table/coreTablesFeature.types.ts:124](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L124) +Defined in: [core/table/coreTablesFeature.types.ts:131](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L131) ## Extended by @@ -29,7 +29,7 @@ Defined in: [core/table/coreTablesFeature.types.ts:124](https://github.com/TanSt optional _cellPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:135](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L135) +Defined in: [core/table/coreTablesFeature.types.ts:142](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L142) Prototype cache for Cell objects - shared by all cells in this table @@ -41,7 +41,7 @@ Prototype cache for Cell objects - shared by all cells in this table optional _columnPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:139](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L139) +Defined in: [core/table/coreTablesFeature.types.ts:146](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L146) Prototype cache for Column objects - shared by all columns in this table @@ -53,7 +53,7 @@ Prototype cache for Column objects - shared by all columns in this table readonly _features: Partial & TFeatures; ``` -Defined in: [core/table/coreTablesFeature.types.ts:143](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L143) +Defined in: [core/table/coreTablesFeature.types.ts:150](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L150) The features that are enabled for the table. @@ -65,7 +65,7 @@ The features that are enabled for the table. optional _headerPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:147](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L147) +Defined in: [core/table/coreTablesFeature.types.ts:154](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L154) Prototype cache for Header objects - shared by all headers in this table @@ -77,7 +77,7 @@ Prototype cache for Header objects - shared by all headers in this table readonly _reactivity: TableReactivityBindings; ``` -Defined in: [core/table/coreTablesFeature.types.ts:131](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L131) +Defined in: [core/table/coreTablesFeature.types.ts:138](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L138) Table reactivity bindings for interacting with TanStack Store. @@ -89,7 +89,7 @@ Table reactivity bindings for interacting with TanStack Store. readonly _rowModelFns: RowModelFns; ``` -Defined in: [core/table/coreTablesFeature.types.ts:151](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L151) +Defined in: [core/table/coreTablesFeature.types.ts:158](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L158) The row model processing functions that are used to process the data by features. @@ -101,7 +101,7 @@ The row model processing functions that are used to process the data by features readonly _rowModels: CachedRowModels; ``` -Defined in: [core/table/coreTablesFeature.types.ts:155](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L155) +Defined in: [core/table/coreTablesFeature.types.ts:162](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L162) The row models that are enabled for the table. @@ -113,7 +113,7 @@ The row models that are enabled for the table. optional _rowPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:159](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L159) +Defined in: [core/table/coreTablesFeature.types.ts:166](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L166) Prototype cache for Row objects - shared by all rows in this table @@ -125,7 +125,7 @@ Prototype cache for Row objects - shared by all rows in this table readonly atoms: Atoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:165](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L165) +Defined in: [core/table/coreTablesFeature.types.ts:172](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L172) The readonly derived atoms for each `TableState` slice. Each derives from its corresponding `baseAtom` plus, optionally, a per-slice external atom or @@ -139,7 +139,7 @@ external state value (precedence: external atom > external state > base atom). readonly baseAtoms: BaseAtoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:170](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L170) +Defined in: [core/table/coreTablesFeature.types.ts:177](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L177) The internal writable atoms for each `TableState` slice. This is the library's single write surface — all state mutations from features land here. @@ -152,7 +152,7 @@ single write surface — all state mutations from features land here. readonly initialState: TableState; ``` -Defined in: [core/table/coreTablesFeature.types.ts:174](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L174) +Defined in: [core/table/coreTablesFeature.types.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L181) This is the resolved initial state of the table. @@ -164,7 +164,7 @@ This is the resolved initial state of the table. readonly options: TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:178](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L178) +Defined in: [core/table/coreTablesFeature.types.ts:185](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L185) A read-only reference to the table's current options. @@ -176,7 +176,7 @@ A read-only reference to the table's current options. readonly optional optionsStore: Atom>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:184](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L184) +Defined in: [core/table/coreTablesFeature.types.ts:191](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L191) Writable atom for table options. Only created when `createOptionsStore` is true on the active core reactivity bindings. Adapters that opt out keep @@ -190,7 +190,7 @@ options as plain resolved data instead of backing them with an atom. readonly store: ReadonlyStore>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L189) +Defined in: [core/table/coreTablesFeature.types.ts:196](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L196) The readonly flat store for the table state. Derives from `table.atoms` only; never reads external state directly. diff --git a/docs/reference/index/interfaces/Table_Table.md b/docs/reference/index/interfaces/Table_Table.md index 04b9454597..683479de1a 100644 --- a/docs/reference/index/interfaces/Table_Table.md +++ b/docs/reference/index/interfaces/Table_Table.md @@ -5,7 +5,7 @@ title: Table_Table # Interface: Table\_Table\ -Defined in: [core/table/coreTablesFeature.types.ts:192](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L192) +Defined in: [core/table/coreTablesFeature.types.ts:199](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L199) ## Extends @@ -29,7 +29,7 @@ Defined in: [core/table/coreTablesFeature.types.ts:192](https://github.com/TanSt optional _cellPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:135](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L135) +Defined in: [core/table/coreTablesFeature.types.ts:142](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L142) Prototype cache for Cell objects - shared by all cells in this table @@ -45,7 +45,7 @@ Prototype cache for Cell objects - shared by all cells in this table optional _columnPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:139](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L139) +Defined in: [core/table/coreTablesFeature.types.ts:146](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L146) Prototype cache for Column objects - shared by all columns in this table @@ -61,7 +61,7 @@ Prototype cache for Column objects - shared by all columns in this table readonly _features: Partial & TFeatures; ``` -Defined in: [core/table/coreTablesFeature.types.ts:143](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L143) +Defined in: [core/table/coreTablesFeature.types.ts:150](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L150) The features that are enabled for the table. @@ -77,7 +77,7 @@ The features that are enabled for the table. optional _headerPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:147](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L147) +Defined in: [core/table/coreTablesFeature.types.ts:154](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L154) Prototype cache for Header objects - shared by all headers in this table @@ -93,7 +93,7 @@ Prototype cache for Header objects - shared by all headers in this table readonly _reactivity: TableReactivityBindings; ``` -Defined in: [core/table/coreTablesFeature.types.ts:131](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L131) +Defined in: [core/table/coreTablesFeature.types.ts:138](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L138) Table reactivity bindings for interacting with TanStack Store. @@ -109,7 +109,7 @@ Table reactivity bindings for interacting with TanStack Store. readonly _rowModelFns: RowModelFns; ``` -Defined in: [core/table/coreTablesFeature.types.ts:151](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L151) +Defined in: [core/table/coreTablesFeature.types.ts:158](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L158) The row model processing functions that are used to process the data by features. @@ -125,7 +125,7 @@ The row model processing functions that are used to process the data by features readonly _rowModels: CachedRowModels; ``` -Defined in: [core/table/coreTablesFeature.types.ts:155](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L155) +Defined in: [core/table/coreTablesFeature.types.ts:162](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L162) The row models that are enabled for the table. @@ -141,7 +141,7 @@ The row models that are enabled for the table. optional _rowPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:159](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L159) +Defined in: [core/table/coreTablesFeature.types.ts:166](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L166) Prototype cache for Row objects - shared by all rows in this table @@ -157,7 +157,7 @@ Prototype cache for Row objects - shared by all rows in this table readonly atoms: Atoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:165](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L165) +Defined in: [core/table/coreTablesFeature.types.ts:172](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L172) The readonly derived atoms for each `TableState` slice. Each derives from its corresponding `baseAtom` plus, optionally, a per-slice external atom or @@ -175,7 +175,7 @@ external state value (precedence: external atom > external state > base atom). readonly baseAtoms: BaseAtoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:170](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L170) +Defined in: [core/table/coreTablesFeature.types.ts:177](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L177) The internal writable atoms for each `TableState` slice. This is the library's single write surface — all state mutations from features land here. @@ -192,7 +192,7 @@ single write surface — all state mutations from features land here. readonly initialState: TableState; ``` -Defined in: [core/table/coreTablesFeature.types.ts:174](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L174) +Defined in: [core/table/coreTablesFeature.types.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L181) This is the resolved initial state of the table. @@ -208,7 +208,7 @@ This is the resolved initial state of the table. readonly options: TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:178](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L178) +Defined in: [core/table/coreTablesFeature.types.ts:185](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L185) A read-only reference to the table's current options. @@ -224,7 +224,7 @@ A read-only reference to the table's current options. readonly optional optionsStore: Atom>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:184](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L184) +Defined in: [core/table/coreTablesFeature.types.ts:191](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L191) Writable atom for table options. Only created when `createOptionsStore` is true on the active core reactivity bindings. Adapters that opt out keep @@ -242,7 +242,7 @@ options as plain resolved data instead of backing them with an atom. reset: () => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:203](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L203) +Defined in: [core/table/coreTablesFeature.types.ts:210](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L210) Resets the table's internal base atoms to `table.initialState`. @@ -262,7 +262,7 @@ reset behavior. setOptions: (newOptions) => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:208](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L208) +Defined in: [core/table/coreTablesFeature.types.ts:215](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L215) Updates the table options by applying a value or updater to the current resolved options and then merging them through `options.mergeOptions`. @@ -285,7 +285,7 @@ resolved options and then merging them through `options.mergeOptions`. readonly store: ReadonlyStore>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L189) +Defined in: [core/table/coreTablesFeature.types.ts:196](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L196) The readonly flat store for the table state. Derives from `table.atoms` only; never reads external state directly. diff --git a/docs/reference/index/type-aliases/APIObject.md b/docs/reference/index/type-aliases/APIObject.md index 6929b3d98e..758228617b 100644 --- a/docs/reference/index/type-aliases/APIObject.md +++ b/docs/reference/index/type-aliases/APIObject.md @@ -9,7 +9,7 @@ title: APIObject type APIObject = Record>; ``` -Defined in: [utils.ts:311](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L311) +Defined in: [utils.ts:315](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L315) ## Type Parameters diff --git a/docs/reference/index/type-aliases/PrototypeAPIObject.md b/docs/reference/index/type-aliases/PrototypeAPIObject.md index 5d3af6aafe..2d1fbb9a8b 100644 --- a/docs/reference/index/type-aliases/PrototypeAPIObject.md +++ b/docs/reference/index/type-aliases/PrototypeAPIObject.md @@ -9,7 +9,7 @@ title: PrototypeAPIObject type PrototypeAPIObject = Record>; ``` -Defined in: [utils.ts:366](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L366) +Defined in: [utils.ts:370](https://github.com/TanStack/table/blob/main/packages/table-core/src/utils.ts#L370) ## Type Parameters diff --git a/docs/reference/static-functions/functions/isTouchStartEvent.md b/docs/reference/static-functions/functions/isTouchStartEvent.md index df07a45416..d9fb25aa75 100644 --- a/docs/reference/static-functions/functions/isTouchStartEvent.md +++ b/docs/reference/static-functions/functions/isTouchStartEvent.md @@ -9,7 +9,7 @@ title: isTouchStartEvent function isTouchStartEvent(e): e is TouchEvent; ``` -Defined in: [features/column-resizing/columnResizingFeature.utils.ts:364](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts#L364) +Defined in: [features/column-resizing/columnResizingFeature.utils.ts:363](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts#L363) Narrows an unknown event to a `touchstart` event. diff --git a/docs/reference/static-functions/functions/passiveEventSupported.md b/docs/reference/static-functions/functions/passiveEventSupported.md index c7962b9975..29ad84055f 100644 --- a/docs/reference/static-functions/functions/passiveEventSupported.md +++ b/docs/reference/static-functions/functions/passiveEventSupported.md @@ -9,7 +9,7 @@ title: passiveEventSupported function passiveEventSupported(): boolean; ``` -Defined in: [features/column-resizing/columnResizingFeature.utils.ts:328](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts#L328) +Defined in: [features/column-resizing/columnResizingFeature.utils.ts:329](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts#L329) Detects whether the current environment supports passive event listeners. diff --git a/examples/angular/basic-app-table/package.json b/examples/angular/basic-app-table/package.json index 4b4b7cb703..dc3da4ee65 100644 --- a/examples/angular/basic-app-table/package.json +++ b/examples/angular/basic-app-table/package.json @@ -17,7 +17,9 @@ "@angular/forms": "^21.2.13", "@angular/platform-browser": "^21.2.13", "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", "rxjs": "~7.8.2", "tslib": "^2.8.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 index cbb47d366c..00db520fb7 100644 --- a/examples/angular/basic-app-table/src/app/app.config.ts +++ b/examples/angular/basic-app-table/src/app/app.config.ts @@ -1,6 +1,22 @@ -import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners()], + providers: [ + provideBrowserGlobalErrorListeners(), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], } diff --git a/examples/angular/basic-app-table/src/app/app.html b/examples/angular/basic-app-table/src/app/app.html index bb2de8903e..a5eff916f6 100644 --- a/examples/angular/basic-app-table/src/app/app.html +++ b/examples/angular/basic-app-table/src/app/app.html @@ -1,17 +1,16 @@
- - -
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { @for (header of headerGroup.headers; track header.id) { - @if (!header.isPlaceholder) { - - } + } } @@ -20,8 +19,10 @@ @for (row of table.getRowModel().rows; track row.id) { @for (cell of row.getAllCells(); track cell.id) { - } @@ -31,12 +32,19 @@ @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { @for (footer of footerGroup.headers; track footer.id) { - } }
-
-
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
-
+
+ + {{ renderCell }} +
- {{ footer }} + + @if (!footer.isPlaceholder) { + + {{ footerCell }} + + }
+
+ + Render count: {{ renderCount() }}
diff --git a/examples/angular/basic-app-table/src/app/app.ts b/examples/angular/basic-app-table/src/app/app.ts index cb578b1920..709c8124fa 100644 --- a/examples/angular/basic-app-table/src/app/app.ts +++ b/examples/angular/basic-app-table/src/app/app.ts @@ -1,34 +1,78 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, createTableHook } from '@tanstack/angular-table' -import { makeData } from './makeData' -import type { Person } from './makeData' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' + +// This example uses `createTableHook` to create a reusable Angular table helper instead of independently using `injectTable` and `createColumnHelper`. + +// 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 +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 + _rowModels: {}, 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) +// 5. Define the columns for your table with a stable reference 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`, + 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', @@ -36,7 +80,7 @@ const columns = columnHelper.columns([ footer: (info) => info.column.id, }), columnHelper.accessor('visits', { - header: () => `Visits`, + header: () => 'Visits', footer: (info) => info.column.id, }), columnHelper.accessor('status', { @@ -56,16 +100,25 @@ const columns = columnHelper.columns([ changeDetection: ChangeDetectionStrategy.OnPush, }) export class App { - readonly data = signal>(makeData(20)) + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } + // 6. Store data with a stable reference + readonly data = signal>([...defaultData]) + readonly renderCount = signal(0) - // 6. Create the table instance with the required columns and data. + // 7. 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(() => ({ + key: 'basic-app-table', // needed for devtools + debugTable: true, columns, data: this.data(), - // add additional table options here or in the createTableHook call above })) - refreshData = () => this.data.set(makeData(20)) - stressTest = () => this.data.set(makeData(1_000)) + rerender() { + this.renderCount.update((count) => count + 1) + } } diff --git a/examples/angular/basic-external-atoms/angular.json b/examples/angular/basic-external-atoms/angular.json new file mode 100644 index 0000000000..83757030d4 --- /dev/null +++ b/examples/angular/basic-external-atoms/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { "enabled": false } + }, + "newProjectRoot": "projects", + "projects": { + "basic-external-atoms": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "basic-external-atoms:build:production" + }, + "development": { + "buildTarget": "basic-external-atoms:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/basic-external-atoms/package.json b/examples/angular/basic-external-atoms/package.json new file mode 100644 index 0000000000..72077452f4 --- /dev/null +++ b/examples/angular/basic-external-atoms/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-angular-table-example-basic-external-atoms", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "lint": "eslint ./src", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", + "@tanstack/angular-store": "^0.11.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/basic-external-atoms/src/app/app.config.ts b/examples/angular/basic-external-atoms/src/app/app.config.ts new file mode 100644 index 0000000000..00db520fb7 --- /dev/null +++ b/examples/angular/basic-external-atoms/src/app/app.config.ts @@ -0,0 +1,22 @@ +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], +} diff --git a/examples/angular/basic-external-atoms/src/app/app.html b/examples/angular/basic-external-atoms/src/app/app.html new file mode 100644 index 0000000000..aa23ce81db --- /dev/null +++ b/examples/angular/basic-external-atoms/src/app/app.html @@ -0,0 +1,103 @@ +
+
+ + +
+
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ {{ + headerCell + }} + @if (header.column.getIsSorted() === 'asc') { + 🔼 + } + @if (header.column.getIsSorted() === 'desc') { + 🔽 + } +
+ } +
+ {{ renderCell }} +
+
+
+ + + + +
Page
+ {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of + {{ table.getPageCount().toLocaleString() }}
+ | Go to page: + + +
+
+ + Render count: {{ renderCount() }} +
{{ stringifiedState() }}
+
diff --git a/examples/angular/basic-external-atoms/src/app/app.ts b/examples/angular/basic-external-atoms/src/app/app.ts new file mode 100644 index 0000000000..56a3eac4b1 --- /dev/null +++ b/examples/angular/basic-external-atoms/src/app/app.ts @@ -0,0 +1,110 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + signal, +} from '@angular/core' +import { createAtom } from '@tanstack/angular-store' +import { + FlexRender, + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + injectTable, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' +import { makeData } from './makeData' +import type { PaginationState, SortingState } from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ rowPaginationFeature, rowSortingFeature }) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { header: 'Age' }), + columnHelper.accessor('visits', { header: 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), +]) + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + + effect(() => { + console.log('atom', this.paginationAtom.get()) + }) + } + + readonly data = signal(makeData(1_000)) + readonly renderCount = signal(0) + readonly sortingAtom = createAtom([]) + readonly paginationAtom = createAtom({ + pageIndex: 0, + pageSize: 10, + }) + + readonly table = injectTable(() => ({ + key: 'basic-external-atoms', // needed for devtools + debugTable: true, + _features, + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data(), + atoms: { + sorting: this.sortingAtom, + pagination: this.paginationAtom, + }, + })) + + refreshData() { + this.data.set(makeData(1_000)) + } + + stressTest() { + this.data.set(makeData(200_000)) + } + + rerender() { + this.renderCount.update((count) => count + 1) + } + + onPageInputChange(event: Event) { + const input = event.target as HTMLInputElement + const page = input.value ? Number(input.value) - 1 : 0 + this.table.setPageIndex(page) + } + + onPageSizeChange(event: Event) { + const select = event.target as HTMLSelectElement + this.table.setPageSize(Number(select.value)) + } + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } +} diff --git a/examples/angular/basic-external-atoms/src/app/makeData.ts b/examples/angular/basic-external-atoms/src/app/makeData.ts new file mode 100644 index 0000000000..70d0f2dd79 --- /dev/null +++ b/examples/angular/basic-external-atoms/src/app/makeData.ts @@ -0,0 +1,25 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: 'relationship' | 'complicated' | 'single' + progress: number +} + +const range = (len: number) => Array.from({ length: len }, (_, index) => index) + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + status: faker.helpers.arrayElement(['relationship', 'complicated', 'single']), + progress: faker.number.int(100), +}) + +export function makeData(len: number): Array { + return range(len).map(() => newPerson()) +} diff --git a/examples/angular/basic-external-atoms/src/index.html b/examples/angular/basic-external-atoms/src/index.html new file mode 100644 index 0000000000..2035204fa7 --- /dev/null +++ b/examples/angular/basic-external-atoms/src/index.html @@ -0,0 +1,12 @@ + + + + + Basic External Atoms + + + + + + + diff --git a/examples/angular/basic-external-atoms/src/main.ts b/examples/angular/basic-external-atoms/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/basic-external-atoms/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/basic-external-atoms/src/styles.css b/examples/angular/basic-external-atoms/src/styles.css new file mode 100644 index 0000000000..2a30368984 --- /dev/null +++ b/examples/angular/basic-external-atoms/src/styles.css @@ -0,0 +1,49 @@ +html { + font-family: sans-serif; + font-size: 14px; +} +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} +tbody { + border-bottom: 1px solid lightgray; +} +th, +td { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} +.demo-root { + padding: 0.5rem; +} +.spacer-sm { + height: 0.5rem; +} +.spacer-md { + height: 1rem; +} +.controls, +.inline-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} +.demo-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.5rem; +} +.demo-button-sm { + padding: 0.25rem; +} +.sortable-header { + cursor: pointer; + user-select: none; +} +.page-size-input { + width: 4rem; + padding: 0.25rem; +} diff --git a/examples/angular/basic-external-atoms/tsconfig.app.json b/examples/angular/basic-external-atoms/tsconfig.app.json new file mode 100644 index 0000000000..12855a8e4d --- /dev/null +++ b/examples/angular/basic-external-atoms/tsconfig.app.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/basic-external-atoms/tsconfig.json b/examples/angular/basic-external-atoms/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/basic-external-atoms/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/basic-external-state/angular.json b/examples/angular/basic-external-state/angular.json new file mode 100644 index 0000000000..eb17f3e8c8 --- /dev/null +++ b/examples/angular/basic-external-state/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { "enabled": false } + }, + "newProjectRoot": "projects", + "projects": { + "basic-external-state": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "basic-external-state:build:production" + }, + "development": { + "buildTarget": "basic-external-state:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/basic-external-state/package.json b/examples/angular/basic-external-state/package.json new file mode 100644 index 0000000000..e53bb33aab --- /dev/null +++ b/examples/angular/basic-external-state/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-angular-table-example-basic-external-state", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "lint": "eslint ./src", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", + "@tanstack/angular-store": "^0.11.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/basic-external-state/src/app/app.config.ts b/examples/angular/basic-external-state/src/app/app.config.ts new file mode 100644 index 0000000000..00db520fb7 --- /dev/null +++ b/examples/angular/basic-external-state/src/app/app.config.ts @@ -0,0 +1,22 @@ +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], +} diff --git a/examples/angular/basic-external-state/src/app/app.html b/examples/angular/basic-external-state/src/app/app.html new file mode 100644 index 0000000000..f548aa960b --- /dev/null +++ b/examples/angular/basic-external-state/src/app/app.html @@ -0,0 +1,106 @@ +
+
+ + +
+
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + + @if (header.column.getIsSorted() === 'asc') { + 🔼 + } + @if (header.column.getIsSorted() === 'desc') { + 🔽 + } +
+ } +
+ + {{ renderCell }} + +
+
+
+ + + + + +
Page
+ {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of + {{ table.getPageCount().toLocaleString() }} +
+ + | Go to page: + + + +
+
+ + Render count: {{ renderCount() }} +
{{ stringifiedState() }}
+
diff --git a/examples/angular/basic-external-state/src/app/app.ts b/examples/angular/basic-external-state/src/app/app.ts new file mode 100644 index 0000000000..cb2a5fe4f2 --- /dev/null +++ b/examples/angular/basic-external-state/src/app/app.ts @@ -0,0 +1,121 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + injectTable, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' +import { makeData } from './makeData' +import type { + PaginationState, + SortingState, + Updater, +} from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), +]) + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } + readonly data = signal(makeData(1_000)) + readonly sorting = signal([]) + readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) + readonly renderCount = signal(0) + + readonly table = injectTable(() => ({ + key: 'basic-external-state', // needed for devtools + debugTable: true, + _features, + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data(), + state: { + sorting: this.sorting(), + pagination: this.pagination(), + }, + onSortingChange: (updater: Updater) => { + typeof updater === 'function' + ? this.sorting.update(updater) + : this.sorting.set(updater) + }, + onPaginationChange: (updater: Updater) => { + typeof updater === 'function' + ? this.pagination.update(updater) + : this.pagination.set(updater) + }, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + refreshData() { + this.data.set(makeData(1_000)) + } + + stressTest() { + this.data.set(makeData(200_000)) + } + + rerender() { + this.renderCount.update((count) => count + 1) + } + + onPageInputChange(event: Event) { + const input = event.target as HTMLInputElement + const page = input.value ? Number(input.value) - 1 : 0 + this.table.setPageIndex(page) + } + + onPageSizeChange(event: Event) { + const select = event.target as HTMLSelectElement + this.table.setPageSize(Number(select.value)) + } +} diff --git a/examples/angular/basic-external-state/src/app/makeData.ts b/examples/angular/basic-external-state/src/app/makeData.ts new file mode 100644 index 0000000000..70d0f2dd79 --- /dev/null +++ b/examples/angular/basic-external-state/src/app/makeData.ts @@ -0,0 +1,25 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: 'relationship' | 'complicated' | 'single' + progress: number +} + +const range = (len: number) => Array.from({ length: len }, (_, index) => index) + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + status: faker.helpers.arrayElement(['relationship', 'complicated', 'single']), + progress: faker.number.int(100), +}) + +export function makeData(len: number): Array { + return range(len).map(() => newPerson()) +} diff --git a/examples/angular/basic-external-state/src/index.html b/examples/angular/basic-external-state/src/index.html new file mode 100644 index 0000000000..f7a60244de --- /dev/null +++ b/examples/angular/basic-external-state/src/index.html @@ -0,0 +1,12 @@ + + + + + Basic External State + + + + + + + diff --git a/examples/angular/basic-external-state/src/main.ts b/examples/angular/basic-external-state/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/basic-external-state/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/basic-external-state/src/styles.css b/examples/angular/basic-external-state/src/styles.css new file mode 100644 index 0000000000..70e6fd5b4c --- /dev/null +++ b/examples/angular/basic-external-state/src/styles.css @@ -0,0 +1,60 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th, +td { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +.demo-root { + padding: 0.5rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.inline-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.demo-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} diff --git a/examples/angular/basic-external-state/tsconfig.app.json b/examples/angular/basic-external-state/tsconfig.app.json new file mode 100644 index 0000000000..8426ad9558 --- /dev/null +++ b/examples/angular/basic-external-state/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/basic-external-state/tsconfig.json b/examples/angular/basic-external-state/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/basic-external-state/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/basic-inject-table/package.json b/examples/angular/basic-inject-table/package.json index 97879b00fd..fafbfbb14f 100644 --- a/examples/angular/basic-inject-table/package.json +++ b/examples/angular/basic-inject-table/package.json @@ -18,7 +18,9 @@ "@angular/platform-browser": "^21.2.13", "@angular/router": "^21.2.13", "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", "rxjs": "~7.8.2", "tslib": "^2.8.1" }, diff --git a/examples/angular/basic-inject-table/src/app/app.config.ts b/examples/angular/basic-inject-table/src/app/app.config.ts index 00f6004def..1805a5f5ce 100644 --- a/examples/angular/basic-inject-table/src/app/app.config.ts +++ b/examples/angular/basic-inject-table/src/app/app.config.ts @@ -1,8 +1,25 @@ +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' import { provideRouter } from '@angular/router' -import { provideBrowserGlobalErrorListeners } from '@angular/core' import { routes } from './app.routes' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], + providers: [ + provideBrowserGlobalErrorListeners(), + provideRouter(routes), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], } diff --git a/examples/angular/basic-inject-table/src/app/app.html b/examples/angular/basic-inject-table/src/app/app.html index bb2de8903e..a5eff916f6 100644 --- a/examples/angular/basic-inject-table/src/app/app.html +++ b/examples/angular/basic-inject-table/src/app/app.html @@ -1,17 +1,16 @@
- - -
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { @for (header of headerGroup.headers; track header.id) { - @if (!header.isPlaceholder) { - - } + } } @@ -20,8 +19,10 @@ @for (row of table.getRowModel().rows; track row.id) { @for (cell of row.getAllCells(); track cell.id) { - } @@ -31,12 +32,19 @@ @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { @for (footer of footerGroup.headers; track footer.id) { - } }
-
-
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
-
+
+ + {{ renderCell }} +
- {{ footer }} + + @if (!footer.isPlaceholder) { + + {{ footerCell }} + + }
+
+ + Render count: {{ renderCount() }}
diff --git a/examples/angular/basic-inject-table/src/app/app.ts b/examples/angular/basic-inject-table/src/app/app.ts index 44cce9194c..fe15c1aee7 100644 --- a/examples/angular/basic-inject-table/src/app/app.ts +++ b/examples/angular/basic-inject-table/src/app/app.ts @@ -1,47 +1,91 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table' -import { makeData } from './makeData' -import type { Person } from './makeData' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' import type { ColumnDef } from '@tanstack/angular-table' +// This example uses the Angular standalone `injectTable` helper to create a table without the `createTableHook` util. + +// 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 +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: 12, + 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 _features = tableFeatures({}) // util method to create sharable TFeatures object/type +const _features = tableFeatures({}) // 4. Define the columns for your table. This uses the new `ColumnDef` type to define columns. -// Alternatively, check out the createAppTable/createColumnHelper util for an even more type-safe way to define columns. -const defaultColumns: Array> = [ +// Alternatively, check out the createTableHook/createColumnHelper util for an even more type-safe way to define columns. +const columns: Array> = [ { accessorKey: 'firstName', + header: 'First Name', cell: (info) => info.getValue(), - footer: (info) => info.column.id, }, { accessorFn: (row) => row.lastName, id: 'lastName', - cell: (info) => `${info.getValue()}`, - header: () => `Last Name`, - footer: (info) => info.column.id, + header: () => 'Last Name', + cell: (info) => info.getValue(), }, { - accessorKey: 'age', + accessorFn: (row) => Number(row.age), + id: 'age', header: () => 'Age', - footer: (info) => info.column.id, + cell: (info) => info.renderValue(), }, { accessorKey: 'visits', - header: () => `Visits`, - footer: (info) => info.column.id, + header: () => 'Visits', }, { accessorKey: 'status', header: 'Status', - footer: (info) => info.column.id, }, { accessorKey: 'progress', header: 'Profile Progress', - footer: (info) => info.column.id, }, ] @@ -52,18 +96,26 @@ const defaultColumns: Array> = [ changeDetection: ChangeDetectionStrategy.OnPush, }) export class App { - readonly data = signal>(makeData(20)) + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } + // 5. Store data with a stable reference + readonly data = signal>([...defaultData]) + readonly renderCount = signal(0) - // 5. Create the table instance with required _features, columns, and data - table = injectTable(() => ({ + // 6. Create the table instance with required _features, columns, and data + readonly table = injectTable(() => ({ + key: 'basic-inject-table', // needed for devtools debugTable: true, - _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 + _features, + _rowModels: {}, + columns, data: this.data(), - columns: defaultColumns, - // ...other options here })) - refreshData = () => this.data.set(makeData(20)) - stressTest = () => this.data.set(makeData(1_000)) + rerender() { + this.renderCount.update((count) => count + 1) + } } diff --git a/examples/angular/column-groups/.gitignore b/examples/angular/column-groups/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/column-groups/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-groups/README.md b/examples/angular/column-groups/README.md new file mode 100644 index 0000000000..c53e0b7967 --- /dev/null +++ b/examples/angular/column-groups/README.md @@ -0,0 +1 @@ +# TanStack Angular Table column-groups example diff --git a/examples/angular/column-groups/angular.json b/examples/angular/column-groups/angular.json new file mode 100644 index 0000000000..a47f4d02ed --- /dev/null +++ b/examples/angular/column-groups/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "column-groups": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "column-groups:build:production" + }, + "development": { + "buildTarget": "column-groups:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/column-groups/package.json b/examples/angular/column-groups/package.json new file mode 100644 index 0000000000..c1b8f021c6 --- /dev/null +++ b/examples/angular/column-groups/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-column-groups", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/column-groups/public/favicon.ico b/examples/angular/column-groups/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-groups/public/favicon.ico differ diff --git a/examples/angular/column-groups/src/app/app.config.ts b/examples/angular/column-groups/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/column-groups/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/column-groups/src/app/app.html b/examples/angular/column-groups/src/app/app.html new file mode 100644 index 0000000000..67baaeeaf3 --- /dev/null +++ b/examples/angular/column-groups/src/app/app.html @@ -0,0 +1,52 @@ +
+
+ + +
+
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @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) { + + } + + } + +
+ @if (!header.isPlaceholder) { + {{ + headerCell + }} + } +
+ {{ renderCell }} +
+ @if (!footer.isPlaceholder) { + {{ + footerCell + }} + } +
+
+ +
diff --git a/examples/angular/column-groups/src/app/app.routes.ts b/examples/angular/column-groups/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/column-groups/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/column-groups/src/app/app.ts b/examples/angular/column-groups/src/app/app.ts new file mode 100644 index 0000000000..6b876295a4 --- /dev/null +++ b/examples/angular/column-groups/src/app/app.ts @@ -0,0 +1,85 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { ColumnDef } from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({}) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal>(makeData(20)) + + readonly table = injectTable(() => ({ + _features, + _rowModels: {}, + columns, + data: this.data(), + debugTable: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + refreshData = () => this.data.set(makeData(20)) + stressTest = () => this.data.set(makeData(1_000)) + rerender = () => this.data.update((data) => [...data]) +} diff --git a/examples/angular/column-groups/src/app/makeData.ts b/examples/angular/column-groups/src/app/makeData.ts new file mode 100644 index 0000000000..b9fb014aba --- /dev/null +++ b/examples/angular/column-groups/src/app/makeData.ts @@ -0,0 +1,48 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/column-groups/src/index.html b/examples/angular/column-groups/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/column-groups/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/column-groups/src/main.ts b/examples/angular/column-groups/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/column-groups/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/column-groups/src/styles.css b/examples/angular/column-groups/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/column-groups/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/column-groups/tsconfig.app.json b/examples/angular/column-groups/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/column-groups/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/column-groups/tsconfig.json b/examples/angular/column-groups/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/column-groups/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/column-ordering/src/app/app.html b/examples/angular/column-ordering/src/app/app.html index d2809472ba..17581d135b 100644 --- a/examples/angular/column-ordering/src/app/app.html +++ b/examples/angular/column-ordering/src/app/app.html @@ -93,5 +93,5 @@
-
{{ stringifiedColumnOrdering() }}
+
{{ stringifiedState() }}
diff --git a/examples/angular/column-ordering/src/app/app.ts b/examples/angular/column-ordering/src/app/app.ts index adcd415740..5aa6e34b0a 100644 --- a/examples/angular/column-ordering/src/app/app.ts +++ b/examples/angular/column-ordering/src/app/app.ts @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - signal, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, columnOrderingFeature, @@ -106,9 +101,9 @@ export class App { debugColumns: true, })) - readonly stringifiedColumnOrdering = computed(() => { - return JSON.stringify(this.table.store.state.columnOrder) - }) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } randomizeColumns() { this.table.setColumnOrder( diff --git a/examples/angular/column-pinning-split/.gitignore b/examples/angular/column-pinning-split/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/column-pinning-split/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-pinning-split/README.md b/examples/angular/column-pinning-split/README.md new file mode 100644 index 0000000000..759e7bc738 --- /dev/null +++ b/examples/angular/column-pinning-split/README.md @@ -0,0 +1 @@ +# TanStack Angular Table column-pinning-split example diff --git a/examples/angular/column-pinning-split/angular.json b/examples/angular/column-pinning-split/angular.json new file mode 100644 index 0000000000..0358904330 --- /dev/null +++ b/examples/angular/column-pinning-split/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "column-pinning-split": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "column-pinning-split:build:production" + }, + "development": { + "buildTarget": "column-pinning-split:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/column-pinning-split/package.json b/examples/angular/column-pinning-split/package.json new file mode 100644 index 0000000000..fa2e09cbb9 --- /dev/null +++ b/examples/angular/column-pinning-split/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-column-pinning-split", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/column-pinning-split/public/favicon.ico b/examples/angular/column-pinning-split/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-pinning-split/public/favicon.ico differ diff --git a/examples/angular/column-pinning-split/src/app/app.config.ts b/examples/angular/column-pinning-split/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/column-pinning-split/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/column-pinning-split/src/app/app.html b/examples/angular/column-pinning-split/src/app/app.html new file mode 100644 index 0000000000..3a8a17e74e --- /dev/null +++ b/examples/angular/column-pinning-split/src/app/app.html @@ -0,0 +1,132 @@ +
+ + + +
+ +
+ @for (column of table.getAllLeafColumns(); track column.id) { + + } +
+ +
+ +

+ This example takes advantage of the "splitting" APIs. (APIs that have "left", "center", and + "right" modifiers) +

+ +
+ + + @for (headerGroup of leftHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of visibleRows(); track row.id) { + + @for (cell of row.getLeftVisibleCells(); track cell.id) { + + } + + } + +
+ +
+ + {{ renderCell }} + +
+ + + + @for (headerGroup of centerHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of visibleRows(); track row.id) { + + @for (cell of row.getCenterVisibleCells(); track cell.id) { + + } + + } + +
+ +
+ + {{ renderCell }} + +
+ + + + @for (headerGroup of rightHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of visibleRows(); track row.id) { + + @for (cell of row.getRightVisibleCells(); track cell.id) { + + } + + } + +
+ +
+ + {{ renderCell }} + +
+
+ +
{{ stringifiedState() }}
+
+ + +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+ + @if (!header.isPlaceholder && header.column.getCanPin()) { +
+ @if (header.column.getIsPinned() !== 'left') { + + } + + @if (header.column.getIsPinned()) { + + } + + @if (header.column.getIsPinned() !== 'right') { + + } +
+ } +
diff --git a/examples/angular/column-pinning-split/src/app/app.routes.ts b/examples/angular/column-pinning-split/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/column-pinning-split/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/column-pinning-split/src/app/app.ts b/examples/angular/column-pinning-split/src/app/app.ts new file mode 100644 index 0000000000..f0b0c40faf --- /dev/null +++ b/examples/angular/column-pinning-split/src/app/app.ts @@ -0,0 +1,78 @@ +import { NgTemplateOutlet } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + computed, + signal, +} from '@angular/core' +import { + FlexRender, + columnPinningFeature, + injectTable, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { ColumnDef, ColumnPinningState } from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ columnPinningFeature }) +const columns: Array> = [ + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + }, + { accessorKey: 'age', header: 'Age' }, + { accessorKey: 'visits', header: 'Visits' }, + { accessorKey: 'status', header: 'Status' }, + { accessorKey: 'progress', header: 'Profile Progress' }, +] +@Component({ + selector: 'app-root', + imports: [FlexRender, NgTemplateOutlet], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal(makeData(20)) + readonly columnPinning = signal({ + left: ['firstName'], + right: ['progress'], + }) + readonly table = injectTable(() => ({ + _features, + _rowModels: {}, + columns, + data: this.data(), + state: { + columnPinning: this.columnPinning(), + }, + onColumnPinningChange: (updaterOrValue) => { + typeof updaterOrValue === 'function' + ? this.columnPinning.update(updaterOrValue) + : this.columnPinning.set(updaterOrValue) + }, + debugTable: true, + })) + + readonly leftHeaderGroups = computed(() => this.table.getLeftHeaderGroups()) + readonly centerHeaderGroups = computed(() => + this.table.getCenterHeaderGroups(), + ) + readonly rightHeaderGroups = computed(() => this.table.getRightHeaderGroups()) + readonly visibleRows = computed(() => + this.table.getRowModel().rows.slice(0, 20), + ) + readonly stringifiedState = computed(() => + JSON.stringify(this.table.state, null, 2), + ) + + refreshData = () => this.data.set(makeData(20)) + stressTest = () => this.data.set(makeData(1_000)) +} diff --git a/examples/angular/column-pinning-split/src/app/makeData.ts b/examples/angular/column-pinning-split/src/app/makeData.ts new file mode 100644 index 0000000000..d274a17f45 --- /dev/null +++ b/examples/angular/column-pinning-split/src/app/makeData.ts @@ -0,0 +1,47 @@ +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 +} + +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], + } +} + +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() +} diff --git a/examples/angular/column-pinning-split/src/index.html b/examples/angular/column-pinning-split/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/column-pinning-split/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/column-pinning-split/src/main.ts b/examples/angular/column-pinning-split/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/column-pinning-split/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/column-pinning-split/src/styles.css b/examples/angular/column-pinning-split/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/column-pinning-split/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/column-pinning-split/tsconfig.app.json b/examples/angular/column-pinning-split/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/column-pinning-split/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/column-pinning-split/tsconfig.json b/examples/angular/column-pinning-split/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/column-pinning-split/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/column-pinning-sticky/src/app/app.html b/examples/angular/column-pinning-sticky/src/app/app.html index e961042585..85a1678c3d 100644 --- a/examples/angular/column-pinning-sticky/src/app/app.html +++ b/examples/angular/column-pinning-sticky/src/app/app.html @@ -107,4 +107,4 @@
-
{{ stringifiedColumnPinning() }}
+
{{ stringifiedState() }}
diff --git a/examples/angular/column-pinning-sticky/src/app/app.ts b/examples/angular/column-pinning-sticky/src/app/app.ts index c001307ce2..fd35da5153 100644 --- a/examples/angular/column-pinning-sticky/src/app/app.ts +++ b/examples/angular/column-pinning-sticky/src/app/app.ts @@ -1,4 +1,4 @@ -import { Component, computed, signal } from '@angular/core' +import { Component, signal } from '@angular/core' import { FlexRender, columnOrderingFeature, @@ -97,9 +97,9 @@ export class App { columnResizeMode: 'onChange' as const, })) - stringifiedColumnPinning = computed(() => { - return JSON.stringify(this.table.store.state.columnPinning) - }) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } readonly getCommonPinningStyles = ( column: Column, diff --git a/examples/angular/column-pinning/src/app/app.html b/examples/angular/column-pinning/src/app/app.html index 1762a5ac96..bc3df57e1b 100644 --- a/examples/angular/column-pinning/src/app/app.html +++ b/examples/angular/column-pinning/src/app/app.html @@ -29,7 +29,9 @@
- +
@@ -203,7 +205,7 @@
-
{{ stringifiedColumnPinning() }}
+
{{ stringifiedState() }}
diff --git a/examples/angular/column-pinning/src/app/app.ts b/examples/angular/column-pinning/src/app/app.ts index 34cc9de4b1..4836b699ed 100644 --- a/examples/angular/column-pinning/src/app/app.ts +++ b/examples/angular/column-pinning/src/app/app.ts @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - signal, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRenderDirective, columnOrderingFeature, @@ -126,9 +121,9 @@ export class App { debugColumns: true, })) - stringifiedColumnPinning = computed(() => { - return JSON.stringify(this.table.store.state.columnPinning) - }) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } randomizeColumns() { this.table.setColumnOrder( diff --git a/examples/angular/column-resizing-performant/src/app/app.ts b/examples/angular/column-resizing-performant/src/app/app.ts index e09963e237..442f0d7194 100644 --- a/examples/angular/column-resizing-performant/src/app/app.ts +++ b/examples/angular/column-resizing-performant/src/app/app.ts @@ -10,7 +10,6 @@ import { columnResizingFeature, columnSizingFeature, injectTable, - shallow, tableFeatures, } from '@tanstack/angular-table' import { makeData } from './makeData' @@ -93,10 +92,6 @@ export class App { debugColumns: true, })) - readonly columnSizing = computed(() => this.table.atoms.columnSizing.get(), { - equal: shallow, - }) - /** * Instead of calling `column.getSize()` on every render for every header * and especially every data cell (very expensive), @@ -104,7 +99,7 @@ export class App { * and pass the column sizes down as CSS variables to the element. */ readonly columnSizeVars = computed(() => { - void this.columnSizing() + void this.table.atoms.columnSizing.get() const headers = untracked(() => this.table.getFlatHeaders()) const colSizes: { [key: string]: number } = {} let i = headers.length @@ -119,7 +114,7 @@ export class App { readonly columnSizingDebugInfo = computed(() => JSON.stringify( { - columnSizing: this.columnSizing(), + columnSizing: this.table.atoms.columnSizing.get(), }, null, 2, diff --git a/examples/angular/column-resizing/.gitignore b/examples/angular/column-resizing/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/column-resizing/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-resizing/README.md b/examples/angular/column-resizing/README.md new file mode 100644 index 0000000000..95a24686ce --- /dev/null +++ b/examples/angular/column-resizing/README.md @@ -0,0 +1 @@ +# TanStack Angular Table column-resizing example diff --git a/examples/angular/column-resizing/angular.json b/examples/angular/column-resizing/angular.json new file mode 100644 index 0000000000..c76093f856 --- /dev/null +++ b/examples/angular/column-resizing/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "column-resizing": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "column-resizing:build:production" + }, + "development": { + "buildTarget": "column-resizing:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/column-resizing/package.json b/examples/angular/column-resizing/package.json new file mode 100644 index 0000000000..1104399bb4 --- /dev/null +++ b/examples/angular/column-resizing/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-column-resizing", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/column-resizing/public/favicon.ico b/examples/angular/column-resizing/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-resizing/public/favicon.ico differ diff --git a/examples/angular/column-resizing/src/app/app.config.ts b/examples/angular/column-resizing/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/column-resizing/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/column-resizing/src/app/app.html b/examples/angular/column-resizing/src/app/app.html new file mode 100644 index 0000000000..15afb2a713 --- /dev/null +++ b/examples/angular/column-resizing/src/app/app.html @@ -0,0 +1,170 @@ +
+
+ + +
+
+ + +
+
+
<table/>
+
+
+ + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+
+ + {{ renderCell }} + +
+ +
+
<div/> (relative)
+
+
+
+ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { +
+ @for (header of headerGroup.headers; track header.id) { +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+
+ } +
+ } +
+
+ @for (row of table.getRowModel().rows; track row.id) { +
+ @for (cell of row.getAllCells(); track cell.id) { +
+ + {{ renderCell }} + +
+ } +
+ } +
+
+
+
+
<div/> (absolute positioning)
+
+
+
+ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { +
+ @for (header of headerGroup.headers; track header.id) { +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+
+ } +
+ } +
+
+ @for (row of table.getRowModel().rows; track row.id) { +
+ @for (cell of row.getAllCells(); track cell.id) { +
+ + {{ renderCell }} + +
+ } +
+ } +
+
+
+ +
+ +
{{ stringifiedState() }}
+ diff --git a/examples/angular/column-resizing/src/app/app.routes.ts b/examples/angular/column-resizing/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/column-resizing/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/column-resizing/src/app/app.ts b/examples/angular/column-resizing/src/app/app.ts new file mode 100644 index 0000000000..13f9016715 --- /dev/null +++ b/examples/angular/column-resizing/src/app/app.ts @@ -0,0 +1,125 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + columnResizingFeature, + columnSizingFeature, + injectTable, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { + ColumnDef, + ColumnResizeDirection, + ColumnResizeMode, + Header, +} from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ columnResizingFeature, columnSizingFeature }) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal(makeData(10)) + readonly columnResizeMode = signal('onChange') + readonly columnResizeDirection = signal('ltr') + readonly rerenders = signal(0) + + readonly table = injectTable(() => ({ + _features, + _rowModels: {}, + columns, + data: this.data(), + columnResizeMode: this.columnResizeMode(), + columnResizeDirection: this.columnResizeDirection(), + debugTable: true, + debugHeaders: true, + debugColumns: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + setResizeMode(event: Event) { + this.columnResizeMode.set( + (event.target as HTMLSelectElement).value as ColumnResizeMode, + ) + } + setResizeDirection(event: Event) { + this.columnResizeDirection.set( + (event.target as HTMLSelectElement).value as ColumnResizeDirection, + ) + } + refreshData = () => this.data.set(makeData(10)) + stressTest = () => this.data.set(makeData(100)) + rerender = () => this.rerenders.update((value) => value + 1) + + getResizeTransform(header: Header) { + if (this.columnResizeMode() !== 'onEnd' || !header.column.getIsResizing()) { + return '' + } + + const direction = + this.table.options.columnResizeDirection === 'rtl' ? -1 : 1 + return `translateX(${ + direction * (this.table.atoms.columnResizing.get().deltaOffset ?? 0) + }px)` + } +} diff --git a/examples/angular/column-resizing/src/app/makeData.ts b/examples/angular/column-resizing/src/app/makeData.ts new file mode 100644 index 0000000000..b9fb014aba --- /dev/null +++ b/examples/angular/column-resizing/src/app/makeData.ts @@ -0,0 +1,48 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/column-resizing/src/index.html b/examples/angular/column-resizing/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/column-resizing/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/column-resizing/src/main.ts b/examples/angular/column-resizing/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/column-resizing/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/column-resizing/src/styles.css b/examples/angular/column-resizing/src/styles.css new file mode 100644 index 0000000000..d7f648b224 --- /dev/null +++ b/examples/angular/column-resizing/src/styles.css @@ -0,0 +1,419 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; + border-collapse: collapse; + border-spacing: 0; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.ltr { + right: 0; +} + +.resizer.rtl { + left: 0; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/column-resizing/tsconfig.app.json b/examples/angular/column-resizing/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/column-resizing/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/column-resizing/tsconfig.json b/examples/angular/column-resizing/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/column-resizing/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/column-sizing/.gitignore b/examples/angular/column-sizing/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/column-sizing/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-sizing/README.md b/examples/angular/column-sizing/README.md new file mode 100644 index 0000000000..849bbfc775 --- /dev/null +++ b/examples/angular/column-sizing/README.md @@ -0,0 +1 @@ +# TanStack Angular Table column-sizing example diff --git a/examples/angular/column-sizing/angular.json b/examples/angular/column-sizing/angular.json new file mode 100644 index 0000000000..cb4ef08074 --- /dev/null +++ b/examples/angular/column-sizing/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "column-sizing": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "column-sizing:build:production" + }, + "development": { + "buildTarget": "column-sizing:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/column-sizing/package.json b/examples/angular/column-sizing/package.json new file mode 100644 index 0000000000..8c7ceed9d2 --- /dev/null +++ b/examples/angular/column-sizing/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-column-sizing", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/column-sizing/public/favicon.ico b/examples/angular/column-sizing/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-sizing/public/favicon.ico differ diff --git a/examples/angular/column-sizing/src/app/app.config.ts b/examples/angular/column-sizing/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/column-sizing/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/column-sizing/src/app/app.html b/examples/angular/column-sizing/src/app/app.html new file mode 100644 index 0000000000..46a50d6910 --- /dev/null +++ b/examples/angular/column-sizing/src/app/app.html @@ -0,0 +1,143 @@ +
+
+ + +
+
+
+
Initial Column Sizes
+
+ @for (column of table.getAllColumns(); track column.id) { +
+ +
+ } +
+
+
+
<table/>
+
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+
+ + {{ renderCell }} + +
+
+
+
<div/> (relative)
+
+
+
+ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { +
+ @for (header of headerGroup.headers; track header.id) { +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+
+ } +
+ } +
+
+ @for (row of table.getRowModel().rows; track row.id) { +
+ @for (cell of row.getAllCells(); track cell.id) { +
+ + {{ renderCell }} + +
+ } +
+ } +
+
+
+
+
<div/> (absolute positioning)
+
+
+
+ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { +
+ @for (header of headerGroup.headers; track header.id) { +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+
+ } +
+ } +
+
+ @for (row of table.getRowModel().rows; track row.id) { +
+ @for (cell of row.getAllCells(); track cell.id) { +
+ + {{ renderCell }} + +
+ } +
+ } +
+
+
+
+ +
{{ stringifiedState() }}
+
diff --git a/examples/angular/column-sizing/src/app/app.routes.ts b/examples/angular/column-sizing/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/column-sizing/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/column-sizing/src/app/app.ts b/examples/angular/column-sizing/src/app/app.ts new file mode 100644 index 0000000000..e52766cefa --- /dev/null +++ b/examples/angular/column-sizing/src/app/app.ts @@ -0,0 +1,88 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + columnSizingFeature, + createColumnHelper, + injectTable, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { ColumnDef } from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ columnSizingFeature }) + +const columnHelper = createColumnHelper() + +// This is not the Column Resizing Example, this is a simplified version that just sets static column sizes +const columns: Array> = + columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + size: 120, // initial size + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + size: 120, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + size: 100, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + footer: (props) => props.column.id, + size: 80, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + size: 200, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + size: 200, + }), + ]) + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal>(makeData(20)) + readonly rerenders = signal(0) + + readonly table = injectTable(() => ({ + _features, + _rowModels: {}, + columns, + data: this.data(), + debugTable: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + refreshData = () => this.data.set(makeData(20)) + stressTest = () => this.data.set(makeData(1_000)) + rerender = () => this.rerenders.update((value) => value + 1) + + setColumnSize(columnId: string, event: Event) { + // Don't actually do this to resize columns. This is just for demonstration purposes. + // See the Column Resizing Example for how to do this with dedicated resizing APIs efficiently. + this.table.setColumnSizing({ + ...this.table.atoms.columnSizing.get(), + [columnId]: Number((event.target as HTMLInputElement).value), + }) + } +} diff --git a/examples/angular/column-sizing/src/app/makeData.ts b/examples/angular/column-sizing/src/app/makeData.ts new file mode 100644 index 0000000000..b9fb014aba --- /dev/null +++ b/examples/angular/column-sizing/src/app/makeData.ts @@ -0,0 +1,48 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/column-sizing/src/index.html b/examples/angular/column-sizing/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/column-sizing/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/column-sizing/src/main.ts b/examples/angular/column-sizing/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/column-sizing/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/column-sizing/src/styles.css b/examples/angular/column-sizing/src/styles.css new file mode 100644 index 0000000000..d7f648b224 --- /dev/null +++ b/examples/angular/column-sizing/src/styles.css @@ -0,0 +1,419 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; + border-collapse: collapse; + border-spacing: 0; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.ltr { + right: 0; +} + +.resizer.rtl { + left: 0; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/column-sizing/tsconfig.app.json b/examples/angular/column-sizing/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/column-sizing/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/column-sizing/tsconfig.json b/examples/angular/column-sizing/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/column-sizing/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/column-visibility/src/app/app.html b/examples/angular/column-visibility/src/app/app.html index 4a883ed014..f16565c755 100644 --- a/examples/angular/column-visibility/src/app/app.html +++ b/examples/angular/column-visibility/src/app/app.html @@ -77,5 +77,7 @@
-
{{ stringifiedColumnVisibility() }}
+ +
+
{{ stringifiedState() }}
diff --git a/examples/angular/column-visibility/src/app/app.ts b/examples/angular/column-visibility/src/app/app.ts index 5ae88eff7e..432f7d1701 100644 --- a/examples/angular/column-visibility/src/app/app.ts +++ b/examples/angular/column-visibility/src/app/app.ts @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - signal, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, columnVisibilityFeature, @@ -99,10 +94,11 @@ export class App { debugColumns: true, })) - readonly stringifiedColumnVisibility = computed(() => { - return JSON.stringify(this.table.store.state.columnVisibility) - }) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } refreshData = () => this.data.set(makeData(20)) stressTest = () => this.data.set(makeData(1_000)) + rerender = () => this.data.update((data) => [...data]) } diff --git a/examples/angular/composable-tables/package.json b/examples/angular/composable-tables/package.json index a6e11bad5c..60c5ec872f 100644 --- a/examples/angular/composable-tables/package.json +++ b/examples/angular/composable-tables/package.json @@ -18,7 +18,9 @@ "@angular/platform-browser": "^21.2.13", "@angular/router": "^21.2.13", "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", "rxjs": "~7.8.2", "tslib": "^2.8.1" }, diff --git a/examples/angular/composable-tables/src/app/app.config.ts b/examples/angular/composable-tables/src/app/app.config.ts index cbb47d366c..00db520fb7 100644 --- a/examples/angular/composable-tables/src/app/app.config.ts +++ b/examples/angular/composable-tables/src/app/app.config.ts @@ -1,6 +1,22 @@ -import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners()], + providers: [ + provideBrowserGlobalErrorListeners(), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], } diff --git a/examples/angular/composable-tables/src/app/components/products-table/products-table.ts b/examples/angular/composable-tables/src/app/components/products-table/products-table.ts index b1808695a8..c8bad25b6d 100644 --- a/examples/angular/composable-tables/src/app/components/products-table/products-table.ts +++ b/examples/angular/composable-tables/src/app/components/products-table/products-table.ts @@ -6,6 +6,7 @@ import { TanStackTableCell, TanStackTableHeader, } from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' import { makeProductData } from '../../makeData' import { createAppColumnHelper, injectAppTable } from '../../table' import type { Product } from '../../makeData' @@ -25,6 +26,11 @@ export const productColumnHelper = createAppColumnHelper() changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProductsTable { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } readonly data = signal(makeProductData(1_000)) readonly columns = productColumnHelper.columns([ @@ -56,6 +62,7 @@ export class ProductsTable { ]) table = injectAppTable(() => ({ + key: 'products-table', // needed for devtools columns: this.columns, data: this.data(), getRowId: (row) => row.id, diff --git a/examples/angular/composable-tables/src/app/components/table-components.ts b/examples/angular/composable-tables/src/app/components/table-components.ts index d13783f270..bc6bf88d75 100644 --- a/examples/angular/composable-tables/src/app/components/table-components.ts +++ b/examples/angular/composable-tables/src/app/components/table-components.ts @@ -78,7 +78,8 @@ export class RowCount { Page - {{ (pageIndex() + 1).toLocaleString() }} of {{ pageCount() }} + {{ (table().state.pagination.pageIndex + 1).toLocaleString() }} of + {{ pageCount() }} @@ -87,11 +88,14 @@ export class RowCount { type="number" min="1" [max]="table().getPageCount()" - [value]="pageIndex() + 1" + [value]="table().state.pagination.pageIndex + 1" (change)="onPageChange($event)" /> - @for (size of pageSizes; track size) { } @@ -107,12 +111,6 @@ export class PaginationControls { readonly canPreviousPage = computed(() => this.table().getCanPreviousPage()) readonly canNextPage = computed(() => this.table().getCanNextPage()) - readonly pageIndex = computed( - () => this.table().store.state.pagination.pageIndex, - ) - readonly pageSize = computed( - () => this.table().store.state.pagination.pageSize, - ) readonly pageCount = computed(() => this.table().getPageCount().toLocaleString(), ) diff --git a/examples/angular/composable-tables/src/app/components/users-table/users-table.ts b/examples/angular/composable-tables/src/app/components/users-table/users-table.ts index b0a150513a..7441694757 100644 --- a/examples/angular/composable-tables/src/app/components/users-table/users-table.ts +++ b/examples/angular/composable-tables/src/app/components/users-table/users-table.ts @@ -7,6 +7,7 @@ import { TanStackTableHeader, flexRenderComponent, } from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' import { makeData } from '../../makeData' import { createAppColumnHelper, injectAppTable } from '../../table' import type { Person } from '../../makeData' @@ -26,6 +27,11 @@ export const personColumnHelper = createAppColumnHelper() changeDetection: ChangeDetectionStrategy.OnPush, }) export class UsersTable { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } readonly data = signal(makeData(1_000)) readonly columns = personColumnHelper.columns([ @@ -67,6 +73,7 @@ export class UsersTable { ]) table = injectAppTable(() => ({ + key: 'users-table', // needed for devtools columns: this.columns, data: this.data(), debugTable: true, diff --git a/examples/angular/custom-plugin/src/app/app.config.ts b/examples/angular/custom-plugin/src/app/app.config.ts index d6d1974987..cbb47d366c 100644 --- a/examples/angular/custom-plugin/src/app/app.config.ts +++ b/examples/angular/custom-plugin/src/app/app.config.ts @@ -1,5 +1,4 @@ import { provideBrowserGlobalErrorListeners } from '@angular/core' - import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { diff --git a/examples/angular/editable/src/app/app.html b/examples/angular/editable/src/app/app.html index 9e8c22936c..9545f1f5f0 100644 --- a/examples/angular/editable/src/app/app.html +++ b/examples/angular/editable/src/app/app.html @@ -55,33 +55,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
diff --git a/examples/angular/expanding/src/app/app.html b/examples/angular/expanding/src/app/app.html index 08f2f79685..b1d1a0bce3 100644 --- a/examples/angular/expanding/src/app/app.html +++ b/examples/angular/expanding/src/app/app.html @@ -53,33 +53,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -87,21 +87,19 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { } - -
{{ rawExpandedState() }}
- -
{{ rawRowSelectionState() }}
+ +
{{ stringifiedState() }}
diff --git a/examples/angular/expanding/src/app/app.ts b/examples/angular/expanding/src/app/app.ts index 8670a8e65b..a7bd31d644 100644 --- a/examples/angular/expanding/src/app/app.ts +++ b/examples/angular/expanding/src/app/app.ts @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - signal, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, createExpandedRowModel, @@ -13,7 +8,6 @@ import { rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, - shallow, tableFeatures, } from '@tanstack/angular-table' import { ReactiveFormsModule } from '@angular/forms' @@ -102,19 +96,9 @@ export class App { debugTable: true, })) - readonly rawExpandedState = computed(() => - JSON.stringify(this.expanded(), undefined, 2), - ) - - readonly rowSelectionState = computed( - () => this.table.atoms.rowSelection.get(), - { - equal: shallow, - }, - ) - readonly rawRowSelectionState = computed(() => - JSON.stringify(this.rowSelectionState(), undefined, 2), - ) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } onPageInputChange(event: Event): void { const inputElement = event.target as HTMLInputElement diff --git a/examples/angular/filters-faceted/.gitignore b/examples/angular/filters-faceted/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/filters-faceted/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/filters-faceted/README.md b/examples/angular/filters-faceted/README.md new file mode 100644 index 0000000000..8ef8d82a8e --- /dev/null +++ b/examples/angular/filters-faceted/README.md @@ -0,0 +1 @@ +# TanStack Angular Table filters-faceted example diff --git a/examples/angular/filters-faceted/angular.json b/examples/angular/filters-faceted/angular.json new file mode 100644 index 0000000000..2ed02478e8 --- /dev/null +++ b/examples/angular/filters-faceted/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "filters-faceted": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "filters-faceted:build:production" + }, + "development": { + "buildTarget": "filters-faceted:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/filters-faceted/package.json b/examples/angular/filters-faceted/package.json new file mode 100644 index 0000000000..a542882b1d --- /dev/null +++ b/examples/angular/filters-faceted/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-angular-table-example-filters-faceted", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "lint": "eslint ./src", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-pacer": "^0.23.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/filters-faceted/public/favicon.ico b/examples/angular/filters-faceted/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/filters-faceted/public/favicon.ico differ diff --git a/examples/angular/filters-faceted/src/app/app.config.ts b/examples/angular/filters-faceted/src/app/app.config.ts new file mode 100644 index 0000000000..cbb47d366c --- /dev/null +++ b/examples/angular/filters-faceted/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners()], +} diff --git a/examples/angular/filters-faceted/src/app/app.html b/examples/angular/filters-faceted/src/app/app.html new file mode 100644 index 0000000000..8df5c2edf9 --- /dev/null +++ b/examples/angular/filters-faceted/src/app/app.html @@ -0,0 +1,104 @@ +
+ + +
+ + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + +
+ + @if (header.column.getCanFilter()) { +
+ +
+ } + } +
+ + {{ renderCell }} + +
+ +
+
+ + + + + +
Page
+ + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of + {{ table.getPageCount().toLocaleString() }} + +
+ + | Go to page: + + + + +
+
{{ table.getPrePaginatedRowModel().rows.length.toLocaleString() }} Rows
+
+
{{ stringifiedState() }}
+
+
diff --git a/examples/angular/filters-faceted/src/app/app.routes.ts b/examples/angular/filters-faceted/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/filters-faceted/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/filters-faceted/src/app/app.ts b/examples/angular/filters-faceted/src/app/app.ts new file mode 100644 index 0000000000..4781c26c36 --- /dev/null +++ b/examples/angular/filters-faceted/src/app/app.ts @@ -0,0 +1,118 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createPaginatedRowModel, + createTableHook, + filterFns, + isFunction, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import { TableFilter } from './table-filter/table-filter' +import type { ColumnFiltersState, Updater } from '@tanstack/angular-table' +import type { Person } from './makeData' + +export const _features = tableFeatures({ + columnFilteringFeature, + columnFacetingFeature, + rowPaginationFeature, +}) + +const { injectAppTable, createAppColumnHelper } = createTableHook({ + _features, + _rowModels: { + facetedMinMaxValues: createFacetedMinMaxValues(), + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + filteredRowModel: createFilteredRowModel(filterFns), + paginatedRowModel: createPaginatedRowModel(), + }, + debugTable: true, + debugHeaders: true, + debugColumns: false, +}) + +const columnHelper = createAppColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }), + columnHelper.accessor('age', { + header: () => 'Age', + meta: { + filterVariant: 'range', + }, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + meta: { + filterVariant: 'range', + }, + }), + columnHelper.accessor('status', { + header: 'Status', + meta: { + filterVariant: 'select', + }, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + meta: { + filterVariant: 'range', + }, + }), +]) + +@Component({ + selector: 'app-root', + imports: [TableFilter, FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly columnFilters = signal([]) + readonly data = signal(makeData(1_000)) + + table = injectAppTable(() => ({ + columns, + data: this.data(), + state: { + columnFilters: this.columnFilters(), + }, + onColumnFiltersChange: (updater: Updater) => { + isFunction(updater) + ? this.columnFilters.update(updater) + : this.columnFilters.set(updater) + }, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + onPageInputChange(event: Event): void { + const inputElement = event.target as HTMLInputElement + const page = inputElement.value ? Number(inputElement.value) - 1 : 0 + this.table.setPageIndex(page) + } + + onPageSizeChange(event: any): void { + this.table.setPageSize(Number(event.target.value)) + } + + refreshData = () => this.data.set(makeData(1_000)) + stressTest = () => this.data.set(makeData(200_000)) +} diff --git a/examples/angular/filters-faceted/src/app/debounced-input/debounced-input.ts b/examples/angular/filters-faceted/src/app/debounced-input/debounced-input.ts new file mode 100644 index 0000000000..cc83285953 --- /dev/null +++ b/examples/angular/filters-faceted/src/app/debounced-input/debounced-input.ts @@ -0,0 +1,21 @@ +import { Directive, HostListener, input, output } from '@angular/core' +import { injectDebouncedCallback } from '@tanstack/angular-pacer' + +@Directive({ + standalone: true, + selector: 'input[debouncedInput]', +}) +export class DebouncedInput { + readonly debounce = input(500) + readonly changeEvent = output() + + readonly #emitChange = injectDebouncedCallback( + (event: Event) => this.changeEvent.emit(event), + { wait: () => this.debounce() }, + ) + + @HostListener('change', ['$event']) + onChange(event: Event) { + this.#emitChange(event) + } +} diff --git a/examples/angular/filters-faceted/src/app/makeData.ts b/examples/angular/filters-faceted/src/app/makeData.ts new file mode 100644 index 0000000000..b9fb014aba --- /dev/null +++ b/examples/angular/filters-faceted/src/app/makeData.ts @@ -0,0 +1,48 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts b/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts new file mode 100644 index 0000000000..3eff55ffca --- /dev/null +++ b/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts @@ -0,0 +1,150 @@ +import { Component, computed, input } from '@angular/core' +import { DebouncedInput } from '../debounced-input/debounced-input' +import type { _features } from '../app' +import type { Person } from '../makeData' +import type { + CellData, + Column, + RowData, + Table, + TableFeatures, +} from '@tanstack/angular-table' + +declare module '@tanstack/angular-table' { + // allows us to define custom properties for our columns + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} + +@Component({ + selector: 'app-table-filter', + template: ` + @switch (filterVariant()) { + @case ('range') { +
+
+ + + +
+
+
+ } + @case ('select') { + + } + @default { + + @for (value of sortedUniqueValues(); track value) { + + } + + +
+ } + } + `, + imports: [DebouncedInput], +}) +export class TableFilter { + readonly column = input.required>() + + readonly table = input.required>() + + readonly filterVariant = computed(() => { + return (this.column().columnDef.meta ?? {}).filterVariant + }) + + readonly columnFilterValue = computed( + () => this.column().getFilterValue() as any, + ) + + readonly minRangePlaceholder = computed(() => { + return `Min ${ + this.column().getFacetedMinMaxValues()?.[0] !== undefined + ? `(${this.column().getFacetedMinMaxValues()?.[0]})` + : '' + }` + }) + + readonly maxRangePlaceholder = computed(() => { + return `Max ${ + this.column().getFacetedMinMaxValues()?.[1] + ? `(${this.column().getFacetedMinMaxValues()?.[1]})` + : '' + }` + }) + + readonly sortedUniqueValues = computed(() => { + const filterVariant = this.filterVariant() + const column = this.column() + if (filterVariant === 'range') { + return [] + } + return Array.from(column.getFacetedUniqueValues().keys()) + .sort() + .slice(0, 5000) + }) + + readonly changeMinRangeValue = (event: Event) => { + const value = (event.target as HTMLInputElement).value + this.column().setFilterValue((old?: [number, number]) => { + return [value, old?.[1]] + }) + } + + readonly changeMaxRangeValue = (event: Event) => { + const value = (event.target as HTMLInputElement).value + this.column().setFilterValue((old?: [number, number]) => { + return [old?.[0], value] + }) + } +} diff --git a/examples/angular/filters-faceted/src/index.html b/examples/angular/filters-faceted/src/index.html new file mode 100644 index 0000000000..4f40d38cef --- /dev/null +++ b/examples/angular/filters-faceted/src/index.html @@ -0,0 +1,13 @@ + + + + + Selection + + + + + + + + diff --git a/examples/angular/filters-faceted/src/main.ts b/examples/angular/filters-faceted/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/filters-faceted/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/filters-faceted/src/styles.css b/examples/angular/filters-faceted/src/styles.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/angular/filters-faceted/src/styles.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/filters-faceted/tsconfig.app.json b/examples/angular/filters-faceted/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/filters-faceted/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/filters-faceted/tsconfig.json b/examples/angular/filters-faceted/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/filters-faceted/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/filters-fuzzy/.gitignore b/examples/angular/filters-fuzzy/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/filters-fuzzy/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/filters-fuzzy/README.md b/examples/angular/filters-fuzzy/README.md new file mode 100644 index 0000000000..d784d7cbee --- /dev/null +++ b/examples/angular/filters-fuzzy/README.md @@ -0,0 +1 @@ +# TanStack Angular Table filters-fuzzy example diff --git a/examples/angular/filters-fuzzy/angular.json b/examples/angular/filters-fuzzy/angular.json new file mode 100644 index 0000000000..e9ed3416e3 --- /dev/null +++ b/examples/angular/filters-fuzzy/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "filters-fuzzy": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "filters-fuzzy:build:production" + }, + "development": { + "buildTarget": "filters-fuzzy:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/filters-fuzzy/package.json b/examples/angular/filters-fuzzy/package.json new file mode 100644 index 0000000000..1a3c5f3262 --- /dev/null +++ b/examples/angular/filters-fuzzy/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-angular-table-example-filters-fuzzy", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-pacer": "^0.23.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/match-sorter-utils": "workspace:*", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/filters-fuzzy/public/favicon.ico b/examples/angular/filters-fuzzy/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/filters-fuzzy/public/favicon.ico differ diff --git a/examples/angular/filters-fuzzy/src/app/app.config.ts b/examples/angular/filters-fuzzy/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/filters-fuzzy/src/app/app.html b/examples/angular/filters-fuzzy/src/app/app.html new file mode 100644 index 0000000000..bc94c38936 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/app/app.html @@ -0,0 +1,74 @@ +
+ + + +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ {{ + headerCell + }} + @if (header.column.getIsSorted() === 'asc') { + asc + } + @if (header.column.getIsSorted() === 'desc') { + desc + } +
+ } +
+ {{ renderCell }} +
+
+ + + +
+
{{ stringifiedState() }}
+
diff --git a/examples/angular/filters-fuzzy/src/app/app.routes.ts b/examples/angular/filters-fuzzy/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/filters-fuzzy/src/app/app.ts b/examples/angular/filters-fuzzy/src/app/app.ts new file mode 100644 index 0000000000..ea5905c109 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/app/app.ts @@ -0,0 +1,118 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { compareItems, rankItem } from '@tanstack/match-sorter-utils' +import { + FlexRender, + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFns, + globalFilteringFeature, + injectTable, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { DebouncedInput } from './debounced-input/debounced-input' +import { makeData } from './makeData' +import type { FilterFn, SortFn } from '@tanstack/angular-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import type { Person } from './makeData' + +const _features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + rowPaginationFeature, +}) +const columnHelper = createColumnHelper() + +declare module '@tanstack/angular-table' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank?: RankingInfo + } +} + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + const itemRank = rankItem(row.getValue(columnId), value) + addMeta?.({ itemRank }) + return itemRank.passed +} + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} + +const columns = columnHelper.columns([ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + filterFn: 'fuzzy', + sortFn: fuzzySort, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + filterFn: 'fuzzy', + sortFn: fuzzySort, + }), + columnHelper.accessor('age', { header: () => 'Age' }), + columnHelper.accessor('visits', { header: () => 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), +]) + +@Component({ + selector: 'app-root', + imports: [FlexRender, DebouncedInput], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal(makeData(1_000)) + readonly table = injectTable(() => ({ + _features, + _rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data: this.data(), + globalFilterFn: 'fuzzy', + debugTable: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + refreshData = () => this.data.set(makeData(1_000)) + stressTest = () => this.data.set(makeData(50_000)) + onGlobalFilter(event: Event) { + this.table.setGlobalFilter((event.target as HTMLInputElement).value) + } + onPageSizeChange(event: Event) { + this.table.setPageSize(Number((event.target as HTMLSelectElement).value)) + } +} diff --git a/examples/angular/filters-fuzzy/src/app/debounced-input/debounced-input.ts b/examples/angular/filters-fuzzy/src/app/debounced-input/debounced-input.ts new file mode 100644 index 0000000000..cc83285953 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/app/debounced-input/debounced-input.ts @@ -0,0 +1,21 @@ +import { Directive, HostListener, input, output } from '@angular/core' +import { injectDebouncedCallback } from '@tanstack/angular-pacer' + +@Directive({ + standalone: true, + selector: 'input[debouncedInput]', +}) +export class DebouncedInput { + readonly debounce = input(500) + readonly changeEvent = output() + + readonly #emitChange = injectDebouncedCallback( + (event: Event) => this.changeEvent.emit(event), + { wait: () => this.debounce() }, + ) + + @HostListener('change', ['$event']) + onChange(event: Event) { + this.#emitChange(event) + } +} diff --git a/examples/angular/filters-fuzzy/src/app/makeData.ts b/examples/angular/filters-fuzzy/src/app/makeData.ts new file mode 100644 index 0000000000..38c1db1f15 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/app/makeData.ts @@ -0,0 +1,45 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + id: number + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (num: number): Person => ({ + id: num, + 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], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (index): Person => ({ + ...newPerson(index), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/angular/filters-fuzzy/src/index.html b/examples/angular/filters-fuzzy/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/filters-fuzzy/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/filters-fuzzy/src/main.ts b/examples/angular/filters-fuzzy/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/filters-fuzzy/src/styles.css b/examples/angular/filters-fuzzy/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/filters-fuzzy/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/filters-fuzzy/tsconfig.app.json b/examples/angular/filters-fuzzy/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/filters-fuzzy/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/filters-fuzzy/tsconfig.json b/examples/angular/filters-fuzzy/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/filters-fuzzy/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/filters/package.json b/examples/angular/filters/package.json index 8364bb341f..7faa2d44ab 100644 --- a/examples/angular/filters/package.json +++ b/examples/angular/filters/package.json @@ -18,7 +18,7 @@ "@angular/platform-browser": "^21.2.13", "@angular/router": "^21.2.13", "@faker-js/faker": "^10.4.0", - "@tanstack/angular-pacer": "^0.23.1", + "@tanstack/angular-pacer": "^0.23.0", "@tanstack/angular-table": "^9.0.0-alpha.49", "rxjs": "~7.8.2", "tslib": "^2.8.1" diff --git a/examples/angular/filters/src/app/app.html b/examples/angular/filters/src/app/app.html index 4a9c219806..8df5c2edf9 100644 --- a/examples/angular/filters/src/app/app.html +++ b/examples/angular/filters/src/app/app.html @@ -51,33 +51,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -85,13 +85,13 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { } @@ -99,6 +99,6 @@
{{ table.getPrePaginatedRowModel().rows.length.toLocaleString() }} Rows
-
{{ stringifiedFilters() }}
+
{{ stringifiedState() }}
diff --git a/examples/angular/filters/src/app/app.ts b/examples/angular/filters/src/app/app.ts index 7cab3f551f..eaf9cf1e74 100644 --- a/examples/angular/filters/src/app/app.ts +++ b/examples/angular/filters/src/app/app.ts @@ -1,16 +1,7 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - signal, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, - columnFacetingFeature, columnFilteringFeature, - createFacetedMinMaxValues, - createFacetedRowModel, - createFacetedUniqueValues, createFilteredRowModel, createPaginatedRowModel, createTableHook, @@ -26,16 +17,12 @@ import type { Person } from './makeData' export const _features = tableFeatures({ columnFilteringFeature, - columnFacetingFeature, rowPaginationFeature, }) const { injectAppTable, createAppColumnHelper } = createTableHook({ _features, _rowModels: { - facetedMinMaxValues: createFacetedMinMaxValues(), - facetedRowModel: createFacetedRowModel(), - facetedUniqueValues: createFacetedUniqueValues(), filteredRowModel: createFilteredRowModel(filterFns), paginatedRowModel: createPaginatedRowModel(), }, @@ -104,9 +91,9 @@ export class App { }, })) - readonly stringifiedFilters = computed(() => - JSON.stringify(this.columnFilters(), null, 2), - ) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } onPageInputChange(event: Event): void { const inputElement = event.target as HTMLInputElement diff --git a/examples/angular/filters/src/app/table-filter/table-filter.ts b/examples/angular/filters/src/app/table-filter/table-filter.ts index 3eff55ffca..203f254e3d 100644 --- a/examples/angular/filters/src/app/table-filter/table-filter.ts +++ b/examples/angular/filters/src/app/table-filter/table-filter.ts @@ -26,33 +26,25 @@ declare module '@tanstack/angular-table' { template: ` @switch (filterVariant()) { @case ('range') { -
-
- - - -
-
+
+ +
} @case ('select') { @@ -62,18 +54,14 @@ declare module '@tanstack/angular-table' { > @for (value of sortedUniqueValues(); track value) { - + } } @default { @for (value of sortedUniqueValues(); track value) { - + } -
} } `, @@ -96,55 +81,34 @@ declare module '@tanstack/angular-table' { }) export class TableFilter { readonly column = input.required>() - readonly table = input.required>() - - readonly filterVariant = computed(() => { - return (this.column().columnDef.meta ?? {}).filterVariant - }) - + readonly filterVariant = computed( + () => (this.column().columnDef.meta ?? {}).filterVariant, + ) readonly columnFilterValue = computed( () => this.column().getFilterValue() as any, ) - readonly minRangePlaceholder = computed(() => { - return `Min ${ - this.column().getFacetedMinMaxValues()?.[0] !== undefined - ? `(${this.column().getFacetedMinMaxValues()?.[0]})` - : '' - }` - }) - - readonly maxRangePlaceholder = computed(() => { - return `Max ${ - this.column().getFacetedMinMaxValues()?.[1] - ? `(${this.column().getFacetedMinMaxValues()?.[1]})` - : '' - }` - }) - readonly sortedUniqueValues = computed(() => { - const filterVariant = this.filterVariant() - const column = this.column() - if (filterVariant === 'range') { - return [] - } - return Array.from(column.getFacetedUniqueValues().keys()) + if (this.filterVariant() === 'range') return [] + const columnId = this.column().id + return Array.from( + new Set( + this.table() + .getPreFilteredRowModel() + .flatRows.map((row) => row.getValue(columnId)), + ), + ) .sort() .slice(0, 5000) }) readonly changeMinRangeValue = (event: Event) => { const value = (event.target as HTMLInputElement).value - this.column().setFilterValue((old?: [number, number]) => { - return [value, old?.[1]] - }) + this.column().setFilterValue((old?: [number, number]) => [value, old?.[1]]) } - readonly changeMaxRangeValue = (event: Event) => { const value = (event.target as HTMLInputElement).value - this.column().setFilterValue((old?: [number, number]) => { - return [old?.[0], value] - }) + this.column().setFilterValue((old?: [number, number]) => [old?.[0], value]) } } diff --git a/examples/angular/grouping/src/app/app.html b/examples/angular/grouping/src/app/app.html index 22da76185c..afd99a4728 100644 --- a/examples/angular/grouping/src/app/app.html +++ b/examples/angular/grouping/src/app/app.html @@ -95,33 +95,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -129,18 +129,21 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { }
{{ table.getRowModel().rows.length.toLocaleString() }} Rows
-
{{ stringifiedGrouping() }}
+
+ +
+
{{ stringifiedState() }}
diff --git a/examples/angular/grouping/src/app/app.ts b/examples/angular/grouping/src/app/app.ts index 9f5aa4aa07..b59198dd24 100644 --- a/examples/angular/grouping/src/app/app.ts +++ b/examples/angular/grouping/src/app/app.ts @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - signal, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { FlexRender, isFunction } from '@tanstack/angular-table' import { columns, injectTable } from './columns' import { makeData } from './makeData' @@ -20,10 +15,6 @@ export class App { readonly data = signal(makeData(1_000)) readonly grouping = signal([]) - readonly stringifiedGrouping = computed(() => - JSON.stringify(this.grouping(), null, 2), - ) - readonly table = injectTable(() => ({ debugTable: true, data: this.data(), @@ -42,6 +33,10 @@ export class App { }, })) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + onPageInputChange(event: any): void { const page = event.target.value ? Number(event.target.value) - 1 : 0 this.table.setPageIndex(page) @@ -53,4 +48,5 @@ export class App { refreshData = () => this.data.set(makeData(1_000)) stressTest = () => this.data.set(makeData(200_000)) + rerender = () => this.data.update((data) => [...data]) } diff --git a/examples/angular/kitchen-sink/package.json b/examples/angular/kitchen-sink/package.json index e59c0597df..b56b2a074e 100644 --- a/examples/angular/kitchen-sink/package.json +++ b/examples/angular/kitchen-sink/package.json @@ -18,7 +18,9 @@ "@angular/platform-browser": "^21.2.13", "@angular/router": "^21.2.13", "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", "@tanstack/match-sorter-utils": "^9.0.0-alpha.4", "rxjs": "~7.8.2", "tslib": "^2.8.1" diff --git a/examples/angular/kitchen-sink/src/app/app.config.ts b/examples/angular/kitchen-sink/src/app/app.config.ts index cbb47d366c..00db520fb7 100644 --- a/examples/angular/kitchen-sink/src/app/app.config.ts +++ b/examples/angular/kitchen-sink/src/app/app.config.ts @@ -1,6 +1,22 @@ -import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners()], + providers: [ + provideBrowserGlobalErrorListeners(), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], } diff --git a/examples/angular/kitchen-sink/src/app/app.html b/examples/angular/kitchen-sink/src/app/app.html index 283d69c0e3..8e376c4885 100644 --- a/examples/angular/kitchen-sink/src/app/app.html +++ b/examples/angular/kitchen-sink/src/app/app.html @@ -8,7 +8,7 @@

Kitchen Sink - All Features

[debounce]="300" class="global-filter-input" placeholder="Fuzzy search all columns..." - [value]="table.store.state.globalFilter ?? ''" + [value]="table.atoms.globalFilter.get() ?? ''" (changeEvent)="onGlobalFilterChange($event)" /> @@ -275,33 +275,33 @@

Kitchen Sink - All Features

(click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (pageIndex() + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -309,15 +309,15 @@

Kitchen Sink - All Features

| Go to page: - + @for (size of [10, 20, 30, 50, 100]; track size) { + } @@ -332,6 +332,6 @@

Kitchen Sink - All Features

Table state (live) -
{{ tableState() }}
+
{{ stringifiedState() }}
diff --git a/examples/angular/kitchen-sink/src/app/app.ts b/examples/angular/kitchen-sink/src/app/app.ts index aa524204ed..b443b2bb53 100644 --- a/examples/angular/kitchen-sink/src/app/app.ts +++ b/examples/angular/kitchen-sink/src/app/app.ts @@ -210,14 +210,9 @@ export class App { debugTable: true, })) - readonly columnSizing = computed(() => this.table.atoms.columnSizing.get()) - readonly columnResizing = computed(() => - this.table.atoms.columnResizing.get(), - ) - readonly columnSizeVars = computed(() => { - void this.columnSizing() - void this.columnResizing() + void this.table.atoms.columnSizing.get() + void this.table.atoms.columnResizing.get() const headers = untracked(() => this.table.getFlatHeaders()) const colSizes: Record = {} for (const header of headers) { @@ -227,9 +222,17 @@ export class App { return colSizes }) - readonly tableState = computed(() => - JSON.stringify(this.table.store.state, null, 2), - ) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + pageIndex() { + return this.table.atoms.pagination.get().pageIndex + } + + pageSize() { + return this.table.atoms.pagination.get().pageSize + } refreshData = () => this.data.set(makeData(1_000)) nestedData = () => this.data.set(makeData(100, 5, 3)) @@ -290,7 +293,7 @@ export class App { } cellClass(cell: Cell) { - const groupingActive = this.table.store.state.grouping.length > 0 + const groupingActive = this.table.atoms.grouping.get().length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn return !groupingActive ? undefined diff --git a/examples/angular/pagination/.gitignore b/examples/angular/pagination/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/pagination/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/pagination/README.md b/examples/angular/pagination/README.md new file mode 100644 index 0000000000..ff43460236 --- /dev/null +++ b/examples/angular/pagination/README.md @@ -0,0 +1 @@ +# TanStack Angular Table pagination example diff --git a/examples/angular/pagination/angular.json b/examples/angular/pagination/angular.json new file mode 100644 index 0000000000..51c46c16cc --- /dev/null +++ b/examples/angular/pagination/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "pagination": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "pagination:build:production" + }, + "development": { + "buildTarget": "pagination:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/pagination/package.json b/examples/angular/pagination/package.json new file mode 100644 index 0000000000..6fa7d4a90c --- /dev/null +++ b/examples/angular/pagination/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-pagination", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/pagination/public/favicon.ico b/examples/angular/pagination/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/pagination/public/favicon.ico differ diff --git a/examples/angular/pagination/src/app/app.config.ts b/examples/angular/pagination/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/pagination/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/pagination/src/app/app.html b/examples/angular/pagination/src/app/app.html new file mode 100644 index 0000000000..b178548f11 --- /dev/null +++ b/examples/angular/pagination/src/app/app.html @@ -0,0 +1,116 @@ +
+ + +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @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) { + + } + + } + +
+
+ @if (!header.isPlaceholder) { + {{ + headerCell + }} + } +
+
+ {{ renderCell }} +
+ @if (!footer.isPlaceholder) { + {{ + footerCell + }} + } +
+
+ +
+ + + + +
Page
+ {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of + {{ table.getPageCount().toLocaleString() }}
+ | Go to page: + + +
+ +
+ Showing {{ table.getRowModel().rows.length.toLocaleString() }} of + {{ table.getRowCount().toLocaleString() }} Rows +
+
{{ stringifiedState() }}
+
+
+
+ +
diff --git a/examples/angular/pagination/src/app/app.routes.ts b/examples/angular/pagination/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/pagination/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/pagination/src/app/app.ts b/examples/angular/pagination/src/app/app.ts new file mode 100644 index 0000000000..16d8e810f5 --- /dev/null +++ b/examples/angular/pagination/src/app/app.ts @@ -0,0 +1,104 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + createPaginatedRowModel, + injectTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { ColumnDef } from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ rowPaginationFeature }) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal>(makeData(100_000)) + + readonly table = injectTable(() => ({ + _features, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data(), + debugTable: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + refreshData = () => this.data.set(makeData(100_000)) + stressTest = () => this.data.set(makeData(200_000)) + rerender = () => this.data.update((data) => [...data]) + + onPageInputChange(event: Event): void { + const page = (event.target as HTMLInputElement).value + ? Number((event.target as HTMLInputElement).value) - 1 + : 0 + this.table.setPageIndex(page) + } + + onPageSizeChange(event: Event): void { + this.table.setPageSize(Number((event.target as HTMLSelectElement).value)) + } +} diff --git a/examples/angular/pagination/src/app/makeData.ts b/examples/angular/pagination/src/app/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/angular/pagination/src/app/makeData.ts @@ -0,0 +1,43 @@ +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 +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + 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], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/angular/pagination/src/index.html b/examples/angular/pagination/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/pagination/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/pagination/src/main.ts b/examples/angular/pagination/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/pagination/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/pagination/src/styles.css b/examples/angular/pagination/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/pagination/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/pagination/tsconfig.app.json b/examples/angular/pagination/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/pagination/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/pagination/tsconfig.json b/examples/angular/pagination/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/pagination/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/remote-data/package.json b/examples/angular/remote-data/package.json index 2f85956381..da2444951c 100644 --- a/examples/angular/remote-data/package.json +++ b/examples/angular/remote-data/package.json @@ -12,6 +12,7 @@ "serve:ssr:remote-data": "node dist/remote-data/server/server.mjs" }, "private": true, + "packageManager": "pnpm@11.1.3", "dependencies": { "@angular/common": "^21.2.13", "@angular/compiler": "^21.2.13", diff --git a/examples/angular/remote-data/src/app/app.html b/examples/angular/remote-data/src/app/app.html index 632a612a44..36eca21e8f 100644 --- a/examples/angular/remote-data/src/app/app.html +++ b/examples/angular/remote-data/src/app/app.html @@ -72,33 +72,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -106,13 +106,13 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { } diff --git a/examples/angular/row-pinning/.gitignore b/examples/angular/row-pinning/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/row-pinning/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/row-pinning/README.md b/examples/angular/row-pinning/README.md new file mode 100644 index 0000000000..f712251356 --- /dev/null +++ b/examples/angular/row-pinning/README.md @@ -0,0 +1 @@ +# TanStack Angular Table row-pinning example diff --git a/examples/angular/row-pinning/angular.json b/examples/angular/row-pinning/angular.json new file mode 100644 index 0000000000..68a6be4fa9 --- /dev/null +++ b/examples/angular/row-pinning/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "row-pinning": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "row-pinning:build:production" + }, + "development": { + "buildTarget": "row-pinning:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/row-pinning/package.json b/examples/angular/row-pinning/package.json new file mode 100644 index 0000000000..17c34b6209 --- /dev/null +++ b/examples/angular/row-pinning/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-row-pinning", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/row-pinning/public/favicon.ico b/examples/angular/row-pinning/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/row-pinning/public/favicon.ico differ diff --git a/examples/angular/row-pinning/src/app/app.config.ts b/examples/angular/row-pinning/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/row-pinning/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/row-pinning/src/app/app.html b/examples/angular/row-pinning/src/app/app.html new file mode 100644 index 0000000000..d5dc12b327 --- /dev/null +++ b/examples/angular/row-pinning/src/app/app.html @@ -0,0 +1,123 @@ +
+ + + + + +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + {{ + headerCell + }} + } +
+ @if (cell.column.id === 'pin') { + @if (row.getIsPinned()) { + + } @else { + + } + } @else if (cell.column.id === 'firstName') { +
+ + @if (row.getCanExpand()) { + + } @else { + . + } + {{ + renderCell + }} +
+ } @else { + {{ renderCell }} + } +
+
+ + + + + +
+
{{ stringifiedState() }}
+
diff --git a/examples/angular/row-pinning/src/app/app.routes.ts b/examples/angular/row-pinning/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/row-pinning/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/row-pinning/src/app/app.ts b/examples/angular/row-pinning/src/app/app.ts new file mode 100644 index 0000000000..c3df64e471 --- /dev/null +++ b/examples/angular/row-pinning/src/app/app.ts @@ -0,0 +1,103 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + columnFilteringFeature, + columnSizingFeature, + createExpandedRowModel, + createFilteredRowModel, + createPaginatedRowModel, + filterFns, + injectTable, + isFunction, + rowExpandingFeature, + rowPaginationFeature, + rowPinningFeature, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { + ColumnDef, + ExpandedState, + RowPinningState, + Updater, +} from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ + rowPinningFeature, + rowExpandingFeature, + columnFilteringFeature, + columnSizingFeature, + rowPaginationFeature, +}) +const columns: Array> = [ + { + id: 'pin', + header: 'Pin', + cell: ({ row }) => (row.getIsPinned() ? 'Unpin' : 'Pin'), + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + }, + { accessorKey: 'age', header: 'Age', size: 50 }, + { accessorKey: 'visits', header: 'Visits', size: 50 }, + { accessorKey: 'status', header: 'Status' }, + { accessorKey: 'progress', header: 'Profile Progress', size: 80 }, +] + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal(makeData(1_000, 2, 2)) + readonly rowPinning = signal({ top: [], bottom: [] }) + readonly expanded = signal({}) + readonly keepPinnedRows = signal(true) + readonly includeLeafRows = signal(true) + readonly includeParentRows = signal(false) + + readonly table = injectTable(() => ({ + debugTable: true, + _features, + _rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data(), + initialState: { pagination: { pageSize: 20, pageIndex: 0 } }, + state: { expanded: this.expanded(), rowPinning: this.rowPinning() }, + onExpandedChange: (updater: Updater) => + isFunction(updater) + ? this.expanded.update(updater) + : this.expanded.set(updater), + onRowPinningChange: (updater: Updater) => + isFunction(updater) + ? this.rowPinning.update(updater) + : this.rowPinning.set(updater), + getSubRows: (row) => row.subRows, + keepPinnedRows: this.keepPinnedRows(), + debugAll: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + refreshData = () => this.data.set(makeData(1_000, 2, 2)) + stressTest = () => this.data.set(makeData(200_000, 2, 2)) + onPageSizeChange(event: Event): void { + this.table.setPageSize(Number((event.target as HTMLSelectElement).value)) + } +} diff --git a/examples/angular/row-pinning/src/app/makeData.ts b/examples/angular/row-pinning/src/app/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/angular/row-pinning/src/app/makeData.ts @@ -0,0 +1,43 @@ +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 +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + 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], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/angular/row-pinning/src/index.html b/examples/angular/row-pinning/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/row-pinning/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/row-pinning/src/main.ts b/examples/angular/row-pinning/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/row-pinning/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/row-pinning/src/styles.css b/examples/angular/row-pinning/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/row-pinning/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/row-pinning/tsconfig.app.json b/examples/angular/row-pinning/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/row-pinning/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/row-pinning/tsconfig.json b/examples/angular/row-pinning/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/row-pinning/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/row-selection-signal/package.json b/examples/angular/row-selection-signal/package.json index 802594d65c..f844a7a089 100644 --- a/examples/angular/row-selection-signal/package.json +++ b/examples/angular/row-selection-signal/package.json @@ -10,6 +10,7 @@ "lint": "eslint ./src" }, "private": true, + "packageManager": "pnpm@11.1.3", "dependencies": { "@angular/animations": "^21.2.13", "@angular/common": "^21.2.13", @@ -19,7 +20,9 @@ "@angular/platform-browser": "^21.2.13", "@angular/platform-browser-dynamic": "^21.2.13", "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", "rxjs": "~7.8.2", "tslib": "^2.8.1" }, diff --git a/examples/angular/row-selection-signal/src/app/app.component.html b/examples/angular/row-selection-signal/src/app/app.component.html index 3a59b083f6..2015ee0b7f 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.html +++ b/examples/angular/row-selection-signal/src/app/app.component.html @@ -73,33 +73,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -107,13 +107,13 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { } @@ -133,7 +133,7 @@
-
{{ stringifiedRowSelection() }}
+
{{ stringifiedState() }}
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 55152adb6b..95a209d78f 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.ts +++ b/examples/angular/row-selection-signal/src/app/app.component.ts @@ -17,6 +17,7 @@ import { rowSelectionFeature, tableFeatures, } from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' import { FilterComponent } from './filter' import { makeData } from './makeData' import { @@ -41,6 +42,12 @@ const _features = tableFeatures({ changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } + private readonly rowSelection = signal({}) readonly globalFilter = signal('') readonly data = signal(makeData(1_000)) @@ -108,6 +115,7 @@ export class AppComponent { // TODO make this generic infer without passing in manually table = injectTable(() => ({ + key: 'row-selection-signal', // needed for devtools _features, _rowModels: { filteredRowModel: createFilteredRowModel(filterFns), @@ -130,9 +138,9 @@ export class AppComponent { debugTable: true, })) - readonly stringifiedRowSelection = computed(() => - JSON.stringify(this.rowSelection(), null, 2), - ) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } readonly rowSelectionLength = computed( () => Object.keys(this.rowSelection()).length, diff --git a/examples/angular/row-selection-signal/src/app/app.config.ts b/examples/angular/row-selection-signal/src/app/app.config.ts index f997e614ac..f654e82bfa 100644 --- a/examples/angular/row-selection-signal/src/app/app.config.ts +++ b/examples/angular/row-selection-signal/src/app/app.config.ts @@ -1,5 +1,21 @@ +import { isDevMode } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { - providers: [], + providers: [ + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], } diff --git a/examples/angular/row-selection/package.json b/examples/angular/row-selection/package.json index fa6013158f..c6d5198fa1 100644 --- a/examples/angular/row-selection/package.json +++ b/examples/angular/row-selection/package.json @@ -19,7 +19,9 @@ "@angular/platform-browser": "^21.2.13", "@angular/router": "^21.2.13", "@faker-js/faker": "^10.4.0", + "@tanstack/angular-devtools": "^0.0.4", "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-table-devtools": "^9.0.0-alpha.43", "rxjs": "~7.8.2", "tslib": "^2.8.1" }, diff --git a/examples/angular/row-selection/src/app/app.config.ts b/examples/angular/row-selection/src/app/app.config.ts index cbb47d366c..00db520fb7 100644 --- a/examples/angular/row-selection/src/app/app.config.ts +++ b/examples/angular/row-selection/src/app/app.config.ts @@ -1,6 +1,22 @@ -import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners()], + providers: [ + provideBrowserGlobalErrorListeners(), + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], } diff --git a/examples/angular/row-selection/src/app/app.html b/examples/angular/row-selection/src/app/app.html index 6d27d1c5e4..afca1026d3 100644 --- a/examples/angular/row-selection/src/app/app.html +++ b/examples/angular/row-selection/src/app/app.html @@ -3,10 +3,14 @@ -
- +
+ +
@@ -69,33 +73,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (paginationState().pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -103,13 +107,13 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { } @@ -118,10 +122,13 @@
{{ rowSelectionLength().toLocaleString() }} of - {{ table.getPreFilteredRowModel().rows.length.toLocaleString() }} Total Rows + {{ table.getPreFilteredRowModel().rows.length.toLocaleString() }} Total Rows Selected


+
+ +
-
{{ stringifiedRowSelection() }}
+
{{ stringifiedState() }}
diff --git a/examples/angular/row-selection/src/app/app.ts b/examples/angular/row-selection/src/app/app.ts index e495508fc0..e9516f2fef 100644 --- a/examples/angular/row-selection/src/app/app.ts +++ b/examples/angular/row-selection/src/app/app.ts @@ -8,8 +8,8 @@ import { FlexRender, TanStackTable, flexRenderComponent, - shallow, } from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' import { TableFilter } from './table-filter/table-filter' import { makeData } from './makeData' import { @@ -18,7 +18,7 @@ import { } from './selection-column/selection-column' import { createAppColumnHelper, injectTable } from './table' import type { Person } from './makeData' -import type { RowSelectionState } from '@tanstack/angular-table' +import type { RowSelectionState, Updater } from '@tanstack/angular-table' const columnHelper = createAppColumnHelper() @@ -30,6 +30,11 @@ const columnHelper = createAppColumnHelper() changeDetection: ChangeDetectionStrategy.OnPush, }) export class App { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } private readonly rowSelection = signal({}) readonly globalFilter = signal('') readonly data = signal(makeData(1_000)) @@ -90,10 +95,12 @@ export class App { ]) readonly table = injectTable(() => ({ + key: 'row-selection', // needed for devtools debugTable: true, data: this.data(), columns: this.columns, state: { + globalFilter: this.globalFilter(), rowSelection: this.rowSelection(), }, @@ -106,15 +113,18 @@ export class App { : updaterOrValue, ) }, + onGlobalFilterChange: (updaterOrValue: Updater) => { + this.globalFilter.set( + typeof updaterOrValue === 'function' + ? updaterOrValue(this.globalFilter()) + : updaterOrValue, + ) + }, })) - readonly paginationState = computed(() => this.table.atoms.pagination.get(), { - equal: shallow, - }) - - readonly stringifiedRowSelection = computed(() => - JSON.stringify(this.rowSelection(), null, 2), - ) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } readonly rowSelectionLength = computed( () => Object.keys(this.rowSelection()).length, @@ -139,6 +149,11 @@ export class App { refreshData = () => this.data.set(makeData(1_000)) stressTest = () => this.data.set(makeData(200_000)) + rerender = () => this.data.update((data) => [...data]) + + onGlobalFilter(event: Event): void { + this.table.setGlobalFilter((event.target as HTMLInputElement).value) + } toggleEnableRowSelection() { this.enableRowSelection.update((value) => !value) diff --git a/examples/angular/row-selection/src/app/table.ts b/examples/angular/row-selection/src/app/table.ts index 3810a042ad..95df31a757 100644 --- a/examples/angular/row-selection/src/app/table.ts +++ b/examples/angular/row-selection/src/app/table.ts @@ -4,6 +4,7 @@ import { createPaginatedRowModel, createTableHook, filterFns, + globalFilteringFeature, rowPaginationFeature, rowSelectionFeature, tableFeatures, @@ -11,6 +12,7 @@ import { const _features = tableFeatures({ columnFilteringFeature, + globalFilteringFeature, rowPaginationFeature, rowSelectionFeature, }) diff --git a/examples/angular/signal-input/src/app/person-table/person-table.html b/examples/angular/signal-input/src/app/person-table/person-table.html index ea2246b62c..80b868ef9f 100644 --- a/examples/angular/signal-input/src/app/person-table/person-table.html +++ b/examples/angular/signal-input/src/app/person-table/person-table.html @@ -40,33 +40,33 @@ (click)="table.setPageIndex(0)" [disabled]="!table.getCanPreviousPage()" > - << + <<
Page
- {{ (table.store.state.pagination.pageIndex + 1).toLocaleString() }} of + {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }} of {{ table.getPageCount().toLocaleString() }}
@@ -74,13 +74,13 @@ | Go to page: - @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { } diff --git a/examples/angular/sorting/.gitignore b/examples/angular/sorting/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/sorting/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/sorting/README.md b/examples/angular/sorting/README.md new file mode 100644 index 0000000000..ccdcc822d9 --- /dev/null +++ b/examples/angular/sorting/README.md @@ -0,0 +1 @@ +# TanStack Angular Table sorting example diff --git a/examples/angular/sorting/angular.json b/examples/angular/sorting/angular.json new file mode 100644 index 0000000000..9cf456ad41 --- /dev/null +++ b/examples/angular/sorting/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "sorting": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "sorting:build:production" + }, + "development": { + "buildTarget": "sorting:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/sorting/package.json b/examples/angular/sorting/package.json new file mode 100644 index 0000000000..7b5436e410 --- /dev/null +++ b/examples/angular/sorting/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-angular-table-example-sorting", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/sorting/public/favicon.ico b/examples/angular/sorting/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/sorting/public/favicon.ico differ diff --git a/examples/angular/sorting/src/app/app.config.ts b/examples/angular/sorting/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/sorting/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/sorting/src/app/app.html b/examples/angular/sorting/src/app/app.html new file mode 100644 index 0000000000..f88c48ee42 --- /dev/null +++ b/examples/angular/sorting/src/app/app.html @@ -0,0 +1,65 @@ +
+ + +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows.slice(0, 10); 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) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ {{ + headerCell + }} + {{ sortIndicator(header.column.getIsSorted()) }} +
+ } +
+ {{ renderCell }} +
+ @if (!footer.isPlaceholder) { + {{ + footerCell + }} + } +
+
{{ table.getRowModel().rows.length.toLocaleString() }} Rows
+
+ +
+ +
{{ stringifiedState() }}
+
diff --git a/examples/angular/sorting/src/app/app.routes.ts b/examples/angular/sorting/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/sorting/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/sorting/src/app/app.ts b/examples/angular/sorting/src/app/app.ts new file mode 100644 index 0000000000..f849c363eb --- /dev/null +++ b/examples/angular/sorting/src/app/app.ts @@ -0,0 +1,84 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + FlexRender, + createSortedRowModel, + injectTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import type { ColumnDef, SortFn } from '@tanstack/angular-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ rowSortingFeature }) + +const sortStatusFn: SortFn = (rowA, rowB) => { + const statusOrder = ['single', 'complicated', 'relationship'] + return ( + statusOrder.indexOf(rowA.original.status) - + statusOrder.indexOf(rowB.original.status) + ) +} + +const columns: Array> = [ + { accessorKey: 'firstName', cell: (info) => info.getValue() }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + sortUndefined: 'last', + sortDescFirst: false, + }, + { accessorKey: 'age', header: () => 'Age' }, + { accessorKey: 'visits', header: () => 'Visits', sortUndefined: 'last' }, + { accessorKey: 'status', header: 'Status', sortFn: sortStatusFn }, + { accessorKey: 'progress', header: 'Profile Progress' }, + { accessorKey: 'rank', header: 'Rank', invertSorting: true }, + { accessorKey: 'createdAt', header: 'Created At' }, +] + +const sortIndicators: Record<'asc' | 'desc', string> = { + asc: ' ^', + desc: ' v', +} + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal>(makeData(1_000)) + + readonly table = injectTable(() => ({ + _features, + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data: this.data(), + debugTable: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + refreshData = () => this.data.set(makeData(1_000)) + stressTest = () => this.data.set(makeData(500_000)) + rerender = () => this.data.update((data) => [...data]) + + getSortTitle(canSort: boolean, nextSortOrder: false | 'asc' | 'desc') { + if (!canSort) return undefined + if (nextSortOrder === 'asc') return 'Sort ascending' + if (nextSortOrder === 'desc') return 'Sort descending' + return 'Clear sort' + } + + sortIndicator(sortDirection: false | 'asc' | 'desc') { + return sortDirection ? sortIndicators[sortDirection] : null + } +} diff --git a/examples/angular/sorting/src/app/makeData.ts b/examples/angular/sorting/src/app/makeData.ts new file mode 100644 index 0000000000..fc070cd5d2 --- /dev/null +++ b/examples/angular/sorting/src/app/makeData.ts @@ -0,0 +1,52 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string | undefined + age: number + visits: number | undefined + progress: number + status: 'relationship' | 'complicated' | 'single' + rank: number + createdAt: Date + subRows?: Array +} + +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: Math.random() < 0.1 ? undefined : faker.person.lastName(), + age: faker.number.int(40), + visits: Math.random() < 0.1 ? undefined : faker.number.int(1000), + progress: faker.number.int(100), + createdAt: faker.date.anytime(), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + rank: faker.number.int(100), + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/sorting/src/index.html b/examples/angular/sorting/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/sorting/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/sorting/src/main.ts b/examples/angular/sorting/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/sorting/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/sorting/src/styles.css b/examples/angular/sorting/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/sorting/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/sorting/tsconfig.app.json b/examples/angular/sorting/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/sorting/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/sorting/tsconfig.json b/examples/angular/sorting/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/sorting/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/virtualized-columns/.gitignore b/examples/angular/virtualized-columns/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/virtualized-columns/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/virtualized-columns/README.md b/examples/angular/virtualized-columns/README.md new file mode 100644 index 0000000000..33b6780d83 --- /dev/null +++ b/examples/angular/virtualized-columns/README.md @@ -0,0 +1 @@ +# TanStack Angular Table virtualized-columns example diff --git a/examples/angular/virtualized-columns/angular.json b/examples/angular/virtualized-columns/angular.json new file mode 100644 index 0000000000..09ccb45489 --- /dev/null +++ b/examples/angular/virtualized-columns/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "virtualized-columns": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "virtualized-columns:build:production" + }, + "development": { + "buildTarget": "virtualized-columns:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/virtualized-columns/package.json b/examples/angular/virtualized-columns/package.json new file mode 100644 index 0000000000..e6c7b12565 --- /dev/null +++ b/examples/angular/virtualized-columns/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-angular-table-example-virtualized-columns", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-virtual": "^5.0.1", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/virtualized-columns/public/favicon.ico b/examples/angular/virtualized-columns/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/virtualized-columns/public/favicon.ico differ diff --git a/examples/angular/virtualized-columns/src/app/app.config.ts b/examples/angular/virtualized-columns/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/virtualized-columns/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/virtualized-columns/src/app/app.html b/examples/angular/virtualized-columns/src/app/app.html new file mode 100644 index 0000000000..5b5c0fd3c4 --- /dev/null +++ b/examples/angular/virtualized-columns/src/app/app.html @@ -0,0 +1,76 @@ +
+

+ Notice: You are currently running Angular in development mode. Virtualized + rendering performance will be slightly degraded until this application is built for production. +

+
({{ columns().length.toLocaleString() }} columns)
+
({{ data().length.toLocaleString() }} rows)
+
+ + + +
+
+ + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @if (virtualPaddingLeft()) { + + + } + @for (virtualColumn of virtualColumns(); track virtualColumn.key) { + @let header = headerGroup.headers[virtualColumn.index]; + + } + @if (virtualPaddingRight()) { + + + } + + } + + + @for (virtualRow of virtualRows(); track virtualRow.key) { + @let row = rows()[virtualRow.index]; + @let visibleCells = row.getVisibleCells(); + + @if (virtualPaddingLeft()) { + + + } + @for (virtualColumn of virtualColumns(); track virtualColumn.key) { + @let cell = visibleCells[virtualColumn.index]; + + } + @if (virtualPaddingRight()) { + + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + + {{ sortIndicator(header.column.getIsSorted()) }} +
+ } +
+ + {{ renderCell }} + +
+
+
diff --git a/examples/angular/virtualized-columns/src/app/app.routes.ts b/examples/angular/virtualized-columns/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/virtualized-columns/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/virtualized-columns/src/app/app.ts b/examples/angular/virtualized-columns/src/app/app.ts new file mode 100644 index 0000000000..1479682576 --- /dev/null +++ b/examples/angular/virtualized-columns/src/app/app.ts @@ -0,0 +1,141 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, + viewChild, +} from '@angular/core' +import { + FlexRender, + columnSizingFeature, + columnVisibilityFeature, + createColumnHelper, + createSortedRowModel, + injectTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import { makeColumns, makeData } from './makeData' +import type { ElementRef } from '@angular/core' +import type { Person } from './makeData' + +const _features = tableFeatures({ + columnSizingFeature, + columnVisibilityFeature, + rowSortingFeature, +}) + +const columnHelper = createColumnHelper() + +const DEFAULT_ROW_COUNT = 1_000 +const DEFAULT_COLUMN_COUNT = 1_000 +const STRESS_ROW_COUNT = 10_000 +const STRESS_COLUMN_COUNT = 10_000 + +const makeTableColumns = (columnCount: number) => + columnHelper.columns( + makeColumns(columnCount).map((column) => + columnHelper.accessor(column.accessorKey, { + header: column.header, + size: column.size, + }), + ), + ) + +const sortIndicators: Record = { + asc: ' 🔼', + desc: ' 🔽', +} + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly columns = signal(makeTableColumns(DEFAULT_COLUMN_COUNT)) + readonly data = signal(makeData(DEFAULT_ROW_COUNT, this.columns())) + readonly scrollContainer = + viewChild>('scrollContainer') + + readonly table = injectTable(() => ({ + _features, + _rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns: this.columns(), + data: this.data(), + debugTable: true, + })) + + readonly rows = computed(() => this.table.getRowModel().rows) + readonly visibleColumns = computed(() => this.table.getVisibleLeafColumns()) + + // we are using a slightly different virtualization strategy for columns + // (compared to virtual rows) in order to support dynamic row heights + readonly columnVirtualizer = injectVirtualizer< + HTMLDivElement, + HTMLTableCellElement + >(() => ({ + count: this.visibleColumns().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: (index) => this.visibleColumns()[index].getSize(), // estimate width of each column for accurate scrollbar dragging + horizontal: true, + overscan: 3, // how many columns to render on each side off screen each way (adjust this for performance) + })) + + readonly rowVirtualizer = injectVirtualizer< + HTMLDivElement, + HTMLTableRowElement + >(() => ({ + count: this.rows().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: () => 33, // estimate row height for accurate scrollbar dragging + // measure dynamic row height, except in firefox because it measures table border height incorrectly + measureElement: + typeof window !== 'undefined' && + navigator.userAgent.indexOf('Firefox') === -1 + ? (element) => element.getBoundingClientRect().height + : undefined, + overscan: 5, + })) + + readonly virtualColumns = computed(() => + this.columnVirtualizer.getVirtualItems(), + ) + readonly virtualRows = computed(() => this.rowVirtualizer.getVirtualItems()) + readonly totalSize = computed(() => this.rowVirtualizer.getTotalSize()) + + readonly virtualPaddingLeft = computed(() => { + const virtualColumns = this.virtualColumns() + return virtualColumns.length ? (virtualColumns[0]?.start ?? 0) : undefined + }) + + readonly virtualPaddingRight = computed(() => { + const virtualColumns = this.virtualColumns() + return virtualColumns.length + ? this.columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) + : undefined + }) + + refreshData = () => { + const nextColumns = makeTableColumns(DEFAULT_COLUMN_COUNT) + this.columns.set(nextColumns) + this.data.set(makeData(DEFAULT_ROW_COUNT, nextColumns)) + } + + stressTestRows = () => + this.data.set(makeData(STRESS_ROW_COUNT, this.columns())) + + stressTestColumns = () => { + const nextColumns = makeTableColumns(STRESS_COLUMN_COUNT) + this.columns.set(nextColumns) + this.data.set(makeData(this.data().length, nextColumns)) + } + + sortIndicator(sortDirection: false | 'asc' | 'desc') { + return sortDirection ? sortIndicators[sortDirection] : null + } +} diff --git a/examples/angular/virtualized-columns/src/app/makeData.ts b/examples/angular/virtualized-columns/src/app/makeData.ts new file mode 100644 index 0000000000..2b3277cf86 --- /dev/null +++ b/examples/angular/virtualized-columns/src/app/makeData.ts @@ -0,0 +1,19 @@ +import { faker } from '@faker-js/faker' + +export const makeColumns = (num: number) => + [...Array(num)].map((_, i) => { + return { + accessorKey: i.toString(), + header: 'Column ' + i.toString(), + size: Math.floor(Math.random() * 150) + 100, + } + }) + +export const makeData = (num: number, columns: Array) => + [...Array(num)].map(() => ({ + ...Object.fromEntries( + columns.map((col: any) => [col.accessorKey, faker.person.firstName()]), + ), + })) + +export type Person = ReturnType[0] diff --git a/examples/angular/virtualized-columns/src/index.html b/examples/angular/virtualized-columns/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/virtualized-columns/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/virtualized-columns/src/main.ts b/examples/angular/virtualized-columns/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/virtualized-columns/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/virtualized-columns/src/styles.css b/examples/angular/virtualized-columns/src/styles.css new file mode 100644 index 0000000000..720064ab9d --- /dev/null +++ b/examples/angular/virtualized-columns/src/styles.css @@ -0,0 +1,377 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border-collapse: collapse; + border-spacing: 0; + font-family: arial, sans-serif; + table-layout: fixed; +} + +thead { + background: lightgray; +} + +tr { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; + text-align: left; +} + +td { + padding: 6px; +} + +.container { + border: 1px solid lightgray; + margin: 1rem auto; +} + +.app { + margin: 1rem auto; + text-align: center; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/virtualized-columns/tsconfig.app.json b/examples/angular/virtualized-columns/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/virtualized-columns/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/virtualized-columns/tsconfig.json b/examples/angular/virtualized-columns/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/virtualized-columns/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/virtualized-infinite-scrolling/.gitignore b/examples/angular/virtualized-infinite-scrolling/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/virtualized-infinite-scrolling/README.md b/examples/angular/virtualized-infinite-scrolling/README.md new file mode 100644 index 0000000000..49da3b4126 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/README.md @@ -0,0 +1 @@ +# TanStack Angular Table virtualized-infinite-scrolling example diff --git a/examples/angular/virtualized-infinite-scrolling/angular.json b/examples/angular/virtualized-infinite-scrolling/angular.json new file mode 100644 index 0000000000..f1f33d0c61 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "virtualized-infinite-scrolling": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "virtualized-infinite-scrolling:build:production" + }, + "development": { + "buildTarget": "virtualized-infinite-scrolling:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/virtualized-infinite-scrolling/package.json b/examples/angular/virtualized-infinite-scrolling/package.json new file mode 100644 index 0000000000..aea31689a8 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-angular-table-example-virtualized-infinite-scrolling", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-virtual": "^5.0.1", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/virtualized-infinite-scrolling/public/favicon.ico b/examples/angular/virtualized-infinite-scrolling/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/virtualized-infinite-scrolling/public/favicon.ico differ diff --git a/examples/angular/virtualized-infinite-scrolling/src/app/app.config.ts b/examples/angular/virtualized-infinite-scrolling/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/virtualized-infinite-scrolling/src/app/app.html b/examples/angular/virtualized-infinite-scrolling/src/app/app.html new file mode 100644 index 0000000000..cec8ab3761 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/app/app.html @@ -0,0 +1,65 @@ +@if (isLoading()) { + Loading... +} @else { +
+

+ Notice: You are currently running Angular in development mode. Virtualized + rendering performance will be slightly degraded until this application is built for + production. +

+ ({{ flatData().length.toLocaleString() }} of {{ totalDBRowCount().toLocaleString() }} rows + fetched) +
+ + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (virtualRow of virtualRows(); track virtualRow.key) { + @let row = rows()[virtualRow.index]; + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + + {{ sortIndicator(header.column.getIsSorted()) }} +
+ } +
+ + {{ renderCell }} + +
+
+ @if (isFetching()) { +
Fetching More...
+ } +
+} diff --git a/examples/angular/virtualized-infinite-scrolling/src/app/app.routes.ts b/examples/angular/virtualized-infinite-scrolling/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/virtualized-infinite-scrolling/src/app/app.ts b/examples/angular/virtualized-infinite-scrolling/src/app/app.ts new file mode 100644 index 0000000000..b4c181ed84 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/app/app.ts @@ -0,0 +1,187 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + signal, + viewChild, +} from '@angular/core' +import { + FlexRender, + columnSizingFeature, + createColumnHelper, + createSortedRowModel, + functionalUpdate, + injectTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import { fetchData } from './makeData' +import type { ElementRef } from '@angular/core' +import type { Person, PersonApiResponse } from './makeData' +import type { SortingState, Updater } from '@tanstack/angular-table' + +const fetchSize = 50 + +const _features = tableFeatures({ columnSizingFeature, rowSortingFeature }) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('id', { + header: 'ID', + size: 60, + }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }), + columnHelper.accessor('age', { + header: () => 'Age', + size: 50, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + size: 50, + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + size: 80, + }), + columnHelper.accessor('createdAt', { + header: 'Created At', + cell: (info) => info.getValue().toLocaleString(), + size: 200, + }), +]) + +const sortIndicators: Record = { + asc: ' 🔼', + desc: ' 🔽', +} + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly scrollContainer = + viewChild>('scrollContainer') + readonly sorting = signal([]) + readonly pages = signal>([]) + readonly isFetching = signal(false) + readonly isLoading = signal(true) + private requestId = 0 + + readonly flatData = computed(() => this.pages().flatMap((page) => page.data)) + readonly totalDBRowCount = computed( + () => this.pages()[0]?.meta?.totalRowCount ?? 0, + ) + readonly totalFetched = computed(() => this.flatData().length) + + readonly table = injectTable(() => ({ + _features, + _rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + data: this.flatData(), + columns, + state: { + sorting: this.sorting(), + }, + onSortingChange: (updater) => this.handleSortingChange(updater), + manualSorting: true, + debugTable: true, + })) + + readonly rows = computed(() => this.table.getRowModel().rows) + + readonly rowVirtualizer = injectVirtualizer< + HTMLDivElement, + HTMLTableRowElement + >(() => ({ + count: this.rows().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: () => 33, // estimate row height for accurate scrollbar dragging + // measure dynamic row height, except in firefox because it measures table border height incorrectly + measureElement: + typeof window !== 'undefined' && + navigator.userAgent.indexOf('Firefox') === -1 + ? (element) => element.getBoundingClientRect().height + : undefined, + overscan: 5, + })) + + readonly virtualRows = computed(() => this.rowVirtualizer.getVirtualItems()) + readonly totalSize = computed(() => this.rowVirtualizer.getTotalSize()) + + constructor() { + effect(() => { + this.loadFirstPage(this.sorting()) + }) + + effect(() => { + this.fetchMoreOnBottomReached(this.scrollContainer()?.nativeElement) + }) + } + + async loadFirstPage(sorting: SortingState) { + const requestId = ++this.requestId + this.isLoading.set(true) + this.isFetching.set(true) + const page = await fetchData(0, fetchSize, sorting) + if (requestId !== this.requestId) return + this.pages.set([page]) + this.isLoading.set(false) + this.isFetching.set(false) + } + + async fetchNextPage() { + if (this.isFetching() || this.totalFetched() >= this.totalDBRowCount()) { + return + } + + const requestId = this.requestId + this.isFetching.set(true) + const page = await fetchData( + this.pages().length * fetchSize, + fetchSize, + this.sorting(), + ) + if (requestId !== this.requestId) return + this.pages.update((pages) => [...pages, page]) + this.isFetching.set(false) + } + + // called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table + fetchMoreOnBottomReached(containerRefElement?: HTMLDivElement) { + if (containerRefElement) { + const { scrollHeight, scrollTop, clientHeight } = containerRefElement + // once the user has scrolled within 500px of the bottom of the table, fetch more data if we can + if (scrollHeight - scrollTop - clientHeight < 500) { + void this.fetchNextPage() + } + } + } + + // scroll to top of table when sorting changes + handleSortingChange(updater: Updater) { + this.sorting.set(functionalUpdate(updater, this.sorting())) + if (this.table.getRowModel().rows.length) { + this.rowVirtualizer.scrollToIndex(0) + } + } + + sortIndicator(sortDirection: false | 'asc' | 'desc') { + return sortDirection ? sortIndicators[sortDirection] : null + } +} diff --git a/examples/angular/virtualized-infinite-scrolling/src/app/makeData.ts b/examples/angular/virtualized-infinite-scrolling/src/app/makeData.ts new file mode 100644 index 0000000000..a57832d81d --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/app/makeData.ts @@ -0,0 +1,89 @@ +import { faker } from '@faker-js/faker' +import type { SortingState } from '@tanstack/angular-table' + +export type Person = { + id: number + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + createdAt: Date +} + +export type PersonApiResponse = { + data: Array + meta: { + totalRowCount: number + } +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (index: number): Person => { + return { + id: index + 1, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + createdAt: faker.date.anytime(), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => { + return { + ...newPerson(d), + } + }) + } + + return makeDataLevel() +} + +const data = makeData(1000) + +// simulates a backend api +export const fetchData = async ( + start: number, + size: number, + sorting: SortingState, +) => { + const dbData = [...data] + if (sorting.length) { + const sort = sorting[0] + const { id, desc } = sort as { id: keyof Person; desc: boolean } + dbData.sort((a, b) => { + if (desc) { + return a[id] < b[id] ? 1 : -1 + } + return a[id] > b[id] ? 1 : -1 + }) + } + + // simulate a backend api + await new Promise((resolve) => setTimeout(resolve, 200)) + + return { + data: dbData.slice(start, start + size), + meta: { + totalRowCount: dbData.length, + }, + } +} diff --git a/examples/angular/virtualized-infinite-scrolling/src/index.html b/examples/angular/virtualized-infinite-scrolling/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/virtualized-infinite-scrolling/src/main.ts b/examples/angular/virtualized-infinite-scrolling/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/virtualized-infinite-scrolling/src/styles.css b/examples/angular/virtualized-infinite-scrolling/src/styles.css new file mode 100644 index 0000000000..720064ab9d --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/src/styles.css @@ -0,0 +1,377 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border-collapse: collapse; + border-spacing: 0; + font-family: arial, sans-serif; + table-layout: fixed; +} + +thead { + background: lightgray; +} + +tr { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; + text-align: left; +} + +td { + padding: 6px; +} + +.container { + border: 1px solid lightgray; + margin: 1rem auto; +} + +.app { + margin: 1rem auto; + text-align: center; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/virtualized-infinite-scrolling/tsconfig.app.json b/examples/angular/virtualized-infinite-scrolling/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/virtualized-infinite-scrolling/tsconfig.json b/examples/angular/virtualized-infinite-scrolling/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/virtualized-infinite-scrolling/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/virtualized-rows/.gitignore b/examples/angular/virtualized-rows/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/virtualized-rows/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/virtualized-rows/README.md b/examples/angular/virtualized-rows/README.md new file mode 100644 index 0000000000..49afed859f --- /dev/null +++ b/examples/angular/virtualized-rows/README.md @@ -0,0 +1 @@ +# TanStack Angular Table virtualized-rows example diff --git a/examples/angular/virtualized-rows/angular.json b/examples/angular/virtualized-rows/angular.json new file mode 100644 index 0000000000..ceaa6fa36d --- /dev/null +++ b/examples/angular/virtualized-rows/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "virtualized-rows": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "virtualized-rows:build:production" + }, + "development": { + "buildTarget": "virtualized-rows:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/virtualized-rows/package.json b/examples/angular/virtualized-rows/package.json new file mode 100644 index 0000000000..36683bfb71 --- /dev/null +++ b/examples/angular/virtualized-rows/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-angular-table-example-virtualized-rows", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "@tanstack/angular-virtual": "^5.0.1", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/virtualized-rows/public/favicon.ico b/examples/angular/virtualized-rows/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/virtualized-rows/public/favicon.ico differ diff --git a/examples/angular/virtualized-rows/src/app/app.config.ts b/examples/angular/virtualized-rows/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/virtualized-rows/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/virtualized-rows/src/app/app.html b/examples/angular/virtualized-rows/src/app/app.html new file mode 100644 index 0000000000..a39cda6d64 --- /dev/null +++ b/examples/angular/virtualized-rows/src/app/app.html @@ -0,0 +1,59 @@ +
+

+ Notice: You are currently running Angular in development mode. Virtualized + rendering performance will be slightly degraded until this application is built for production. +

+ ({{ data().length.toLocaleString() }} rows) +
+ + +
+
+ + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (virtualRow of virtualRows(); track virtualRow.key) { + @let row = rows()[virtualRow.index]; + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + + {{ sortIndicator(header.column.getIsSorted()) }} +
+ } +
+ + {{ renderCell }} + +
+
+
+
{{ stringifiedState() }}
diff --git a/examples/angular/virtualized-rows/src/app/app.routes.ts b/examples/angular/virtualized-rows/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/virtualized-rows/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/virtualized-rows/src/app/app.ts b/examples/angular/virtualized-rows/src/app/app.ts new file mode 100644 index 0000000000..cc8bfd3607 --- /dev/null +++ b/examples/angular/virtualized-rows/src/app/app.ts @@ -0,0 +1,115 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, + viewChild, +} from '@angular/core' +import { + FlexRender, + columnSizingFeature, + createColumnHelper, + createSortedRowModel, + injectTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import { makeData } from './makeData' +import type { ElementRef } from '@angular/core' +import type { Person } from './makeData' + +const _features = tableFeatures({ columnSizingFeature, rowSortingFeature }) +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('id', { + header: 'ID', + size: 60, + }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }), + columnHelper.accessor('age', { + header: () => 'Age', + size: 50, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + size: 50, + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + size: 80, + }), + columnHelper.accessor('createdAt', { + header: 'Created At', + cell: (info) => info.getValue().toLocaleString(), + size: 250, + }), +]) + +const sortIndicators: Record = { + asc: ' 🔼', + desc: ' 🔽', +} + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + readonly data = signal(makeData(200_000)) + readonly scrollContainer = + viewChild>('scrollContainer') + + readonly table = injectTable(() => ({ + _features, + _rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data(), + debugTable: true, + })) + + // Important: Keep the virtualizer and the scroll container ref in the same component. + readonly rows = computed(() => this.table.getRowModel().rows) + + // Important: Keep the row virtualizer as close to the rows as possible to avoid unnecessary work. + readonly rowVirtualizer = injectVirtualizer(() => ({ + count: this.rows().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: () => 33, // estimate row height for accurate scrollbar dragging + // measure dynamic row height, except in firefox because it measures table border height incorrectly + measureElement: + typeof window !== 'undefined' && + navigator.userAgent.indexOf('Firefox') === -1 + ? (element) => element.getBoundingClientRect().height + : undefined, + overscan: 5, + })) + + readonly virtualRows = computed(() => this.rowVirtualizer.getVirtualItems()) + readonly totalSize = computed(() => this.rowVirtualizer.getTotalSize()) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + refreshData = () => this.data.set(makeData(200_000)) + stressTest = () => this.data.set(makeData(1_000_000)) + + sortIndicator(sortDirection: false | 'asc' | 'desc') { + return sortDirection ? sortIndicators[sortDirection] : null + } +} diff --git a/examples/angular/virtualized-rows/src/app/makeData.ts b/examples/angular/virtualized-rows/src/app/makeData.ts new file mode 100644 index 0000000000..acd0e474d9 --- /dev/null +++ b/examples/angular/virtualized-rows/src/app/makeData.ts @@ -0,0 +1,41 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + id: number + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + createdAt: Date +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (index: number): Person => ({ + id: index + 1, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + createdAt: faker.date.anytime(), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => ({ ...newPerson(d) })) + } + return makeDataLevel() +} diff --git a/examples/angular/virtualized-rows/src/index.html b/examples/angular/virtualized-rows/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/virtualized-rows/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/virtualized-rows/src/main.ts b/examples/angular/virtualized-rows/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/virtualized-rows/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/virtualized-rows/src/styles.css b/examples/angular/virtualized-rows/src/styles.css new file mode 100644 index 0000000000..720064ab9d --- /dev/null +++ b/examples/angular/virtualized-rows/src/styles.css @@ -0,0 +1,377 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border-collapse: collapse; + border-spacing: 0; + font-family: arial, sans-serif; + table-layout: fixed; +} + +thead { + background: lightgray; +} + +tr { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; + text-align: left; +} + +td { + padding: 6px; +} + +.container { + border: 1px solid lightgray; + margin: 1rem auto; +} + +.app { + margin: 1rem auto; + text-align: center; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/virtualized-rows/tsconfig.app.json b/examples/angular/virtualized-rows/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/virtualized-rows/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/virtualized-rows/tsconfig.json b/examples/angular/virtualized-rows/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/virtualized-rows/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/with-tanstack-form/.gitignore b/examples/angular/with-tanstack-form/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/with-tanstack-form/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/with-tanstack-form/README.md b/examples/angular/with-tanstack-form/README.md new file mode 100644 index 0000000000..007bebcffd --- /dev/null +++ b/examples/angular/with-tanstack-form/README.md @@ -0,0 +1 @@ +# TanStack Angular Table with-tanstack-form example diff --git a/examples/angular/with-tanstack-form/angular.json b/examples/angular/with-tanstack-form/angular.json new file mode 100644 index 0000000000..cf6b54ec10 --- /dev/null +++ b/examples/angular/with-tanstack-form/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "with-tanstack-form": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1.5MB", + "maximumError": "2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "with-tanstack-form:build:production" + }, + "development": { + "buildTarget": "with-tanstack-form:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/with-tanstack-form/package.json b/examples/angular/with-tanstack-form/package.json new file mode 100644 index 0000000000..2334d7a0c2 --- /dev/null +++ b/examples/angular/with-tanstack-form/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-angular-table-example-with-tanstack-form", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-form": "^1.32.0", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/with-tanstack-form/public/favicon.ico b/examples/angular/with-tanstack-form/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/with-tanstack-form/public/favicon.ico differ diff --git a/examples/angular/with-tanstack-form/src/app/app.config.ts b/examples/angular/with-tanstack-form/src/app/app.config.ts new file mode 100644 index 0000000000..47544a0587 --- /dev/null +++ b/examples/angular/with-tanstack-form/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)], +} diff --git a/examples/angular/with-tanstack-form/src/app/app.html b/examples/angular/with-tanstack-form/src/app/app.html new file mode 100644 index 0000000000..7e79a87eab --- /dev/null +++ b/examples/angular/with-tanstack-form/src/app/app.html @@ -0,0 +1,92 @@ +
+ + + + + {{ + validation().success ? 'Valid' : 'Invalid' + }} +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + + + + + + + + } + +
+ @if (!header.isPlaceholder) { + {{ + headerCell + }} + } +
+ + + + + + + + + + + +
+
{{ stringifiedState() }}
+
diff --git a/examples/angular/with-tanstack-form/src/app/app.routes.ts b/examples/angular/with-tanstack-form/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/with-tanstack-form/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/with-tanstack-form/src/app/app.ts b/examples/angular/with-tanstack-form/src/app/app.ts new file mode 100644 index 0000000000..f9b958d31d --- /dev/null +++ b/examples/angular/with-tanstack-form/src/app/app.ts @@ -0,0 +1,115 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, +} from '@angular/core' +import { + FlexRender, + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + filterFns, + injectTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' +import { injectForm, injectStore } from '@tanstack/angular-form' +import { z } from 'zod' +import { makeData } from './makeData' +import type { Person } from './makeData' + +// Define table features +const _features = tableFeatures({ + rowPaginationFeature, + columnFilteringFeature, +}) +const columnHelper = createColumnHelper() + +// Zod validation schema for a person +const personSchema = z.object({ + firstName: z.string().min(1, 'First name is required'), + lastName: z.string().min(1, 'Last name is required'), + age: z + .number() + .min(0, 'Age must be positive') + .max(150, 'Age must be realistic'), + visits: z.number().min(0, 'Visits must be positive'), + progress: z + .number() + .min(0, 'Progress must be 0-100') + .max(100, 'Progress must be 0-100'), + status: z.enum(['relationship', 'complicated', 'single']), +}) + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { header: 'First Name' }), + columnHelper.accessor('lastName', { header: 'Last Name' }), + columnHelper.accessor('age', { header: 'Age' }), + columnHelper.accessor('visits', { header: 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), +]) + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + // Create table using form state as data source. + readonly data = signal(makeData(100)) + readonly table = injectTable(() => ({ + _features, + _rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data(), + debugTable: true, + })) + readonly validation = computed(() => + z.array(personSchema).safeParse(this.data()), + ) + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + update(rowIndex: number, key: keyof Person, event: Event) { + const value = (event.target as HTMLInputElement | HTMLSelectElement).value + this.data.update((rows) => + rows.map((row, index) => + index === rowIndex + ? { + ...row, + [key]: + key === 'age' || key === 'visits' || key === 'progress' + ? Number(value) + : value, + } + : row, + ), + ) + } + addRow() { + this.data.update((rows) => [ + ...rows, + { + firstName: '', + lastName: '', + age: 0, + visits: 0, + progress: 0, + status: 'single', + }, + ]) + } + refreshData = () => this.data.set(makeData(100)) + stressTest = () => this.data.set(makeData(200_000)) + submit() { + alert(`Submitted ${this.data().length} records!`) + } +} diff --git a/examples/angular/with-tanstack-form/src/app/makeData.ts b/examples/angular/with-tanstack-form/src/app/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/angular/with-tanstack-form/src/app/makeData.ts @@ -0,0 +1,48 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/with-tanstack-form/src/index.html b/examples/angular/with-tanstack-form/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/with-tanstack-form/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/with-tanstack-form/src/main.ts b/examples/angular/with-tanstack-form/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/with-tanstack-form/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/with-tanstack-form/src/styles.css b/examples/angular/with-tanstack-form/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/with-tanstack-form/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/with-tanstack-form/tsconfig.app.json b/examples/angular/with-tanstack-form/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/with-tanstack-form/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/with-tanstack-form/tsconfig.json b/examples/angular/with-tanstack-form/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/with-tanstack-form/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/angular/with-tanstack-query/.gitignore b/examples/angular/with-tanstack-query/.gitignore new file mode 100644 index 0000000000..854acd5fc0 --- /dev/null +++ b/examples/angular/with-tanstack-query/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/with-tanstack-query/README.md b/examples/angular/with-tanstack-query/README.md new file mode 100644 index 0000000000..3cccbb6800 --- /dev/null +++ b/examples/angular/with-tanstack-query/README.md @@ -0,0 +1 @@ +# TanStack Angular Table with-tanstack-query example diff --git a/examples/angular/with-tanstack-query/angular.json b/examples/angular/with-tanstack-query/angular.json new file mode 100644 index 0000000000..5d8ccc581b --- /dev/null +++ b/examples/angular/with-tanstack-query/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "with-tanstack-query": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@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": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1.5MB", + "maximumError": "2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "with-tanstack-query:build:production" + }, + "development": { + "buildTarget": "with-tanstack-query:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/examples/angular/with-tanstack-query/package.json b/examples/angular/with-tanstack-query/package.json new file mode 100644 index 0000000000..6bbc93e089 --- /dev/null +++ b/examples/angular/with-tanstack-query/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-angular-table-example-with-tanstack-query", + "scripts": { + "ng": "ng", + "start": "ng serve --port 5173", + "dev": "ng serve --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "lint": "eslint ./src" + }, + "private": true, + "packageManager": "pnpm@11.1.3", + "dependencies": { + "@angular/common": "^21.2.13", + "@angular/compiler": "^21.2.13", + "@angular/core": "^21.2.13", + "@angular/forms": "^21.2.13", + "@angular/platform-browser": "^21.2.13", + "@angular/router": "^21.2.13", + "@faker-js/faker": "^10.4.0", + "@tanstack/angular-query-experimental": "^5.100.14", + "@tanstack/angular-table": "^9.0.0-alpha.49", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^21.2.11", + "@angular/cli": "^21.2.11", + "@angular/compiler-cli": "^21.2.13", + "typescript": "6.0.3" + } +} diff --git a/examples/angular/with-tanstack-query/public/favicon.ico b/examples/angular/with-tanstack-query/public/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/with-tanstack-query/public/favicon.ico differ diff --git a/examples/angular/with-tanstack-query/src/app/app.config.ts b/examples/angular/with-tanstack-query/src/app/app.config.ts new file mode 100644 index 0000000000..993180f4a4 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/app/app.config.ts @@ -0,0 +1,16 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core' +import { + QueryClient, + provideTanStackQuery, +} from '@tanstack/angular-query-experimental' +import { provideRouter } from '@angular/router' +import { routes } from './app.routes' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideRouter(routes), + provideTanStackQuery(new QueryClient()), + ], +} diff --git a/examples/angular/with-tanstack-query/src/app/app.html b/examples/angular/with-tanstack-query/src/app/app.html new file mode 100644 index 0000000000..a88cafdbb4 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/app/app.html @@ -0,0 +1,94 @@ +
+
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + {{ + headerCell + }} + } +
+ {{ renderCell }} +
+
+ + + + +
Page
+ {{ (pagination().pageIndex + 1).toLocaleString() }} of + {{ table.getPageCount().toLocaleString() }}
+ | Go to page: + + + @if (dataQuery.isFetching()) { + Loading... + } +
+
+ Showing {{ table.getRowModel().rows.length.toLocaleString() }} of + {{ dataQuery.data()?.rowCount?.toLocaleString() }} Rows +
+
+ +
+
{{ stringifiedState() }}
+
diff --git a/examples/angular/with-tanstack-query/src/app/app.routes.ts b/examples/angular/with-tanstack-query/src/app/app.routes.ts new file mode 100644 index 0000000000..9a884b1131 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router' + +export const routes: Routes = [] diff --git a/examples/angular/with-tanstack-query/src/app/app.ts b/examples/angular/with-tanstack-query/src/app/app.ts new file mode 100644 index 0000000000..d2a916e8d6 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/app/app.ts @@ -0,0 +1,82 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { + injectQuery, + keepPreviousData, +} from '@tanstack/angular-query-experimental' +import { + FlexRender, + createColumnHelper, + injectTable, + isFunction, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' +import { fetchData } from './fetchData' +import type { PaginationState, Updater } from '@tanstack/angular-table' +import type { Person } from './fetchData' + +const _features = tableFeatures({ rowPaginationFeature }) +const columnHelper = createColumnHelper() +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { header: 'Age' }), + columnHelper.accessor('visits', { header: 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), +]) +const defaultData: Array = [] + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + // Create a stable external signal for the pagination slice. + readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) + + readonly dataQuery = injectQuery(() => ({ + queryKey: ['data', this.pagination()], + queryFn: () => fetchData(this.pagination()), + placeholderData: keepPreviousData, // don't have 0 rows flash while changing pages/loading next page + })) + + readonly table = injectTable(() => ({ + _features, + _rowModels: {}, + columns, + data: this.dataQuery.data()?.rows ?? defaultData, + rowCount: this.dataQuery.data()?.rowCount, + state: { pagination: this.pagination() }, + onPaginationChange: (updater: Updater) => + isFunction(updater) + ? this.pagination.update(updater) + : this.pagination.set(updater), + manualPagination: true, // we're doing manual "server-side" pagination + debugTable: true, + })) + + stringifiedState() { + return JSON.stringify(this.table.state, null, 2) + } + + rerender = () => this.pagination.update((pagination) => ({ ...pagination })) + + onPageInputChange(event: Event): void { + const page = (event.target as HTMLInputElement).value + ? Number((event.target as HTMLInputElement).value) - 1 + : 0 + this.table.setPageIndex(page) + } + onPageSizeChange(event: Event): void { + this.table.setPageSize(Number((event.target as HTMLSelectElement).value)) + } +} diff --git a/examples/angular/with-tanstack-query/src/app/fetchData.ts b/examples/angular/with-tanstack-query/src/app/fetchData.ts new file mode 100644 index 0000000000..eefb050404 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/app/fetchData.ts @@ -0,0 +1,67 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} + +const data = makeData(10000) + +export async function fetchData(options: { + pageIndex: number + pageSize: number +}) { + // Simulate some network latency + await new Promise((r) => setTimeout(r, 500)) + + return { + rows: data.slice( + options.pageIndex * options.pageSize, + (options.pageIndex + 1) * options.pageSize, + ), + pageCount: Math.ceil(data.length / options.pageSize), + rowCount: data.length, + } +} diff --git a/examples/angular/with-tanstack-query/src/app/makeData.ts b/examples/angular/with-tanstack-query/src/app/makeData.ts new file mode 100644 index 0000000000..b9fb014aba --- /dev/null +++ b/examples/angular/with-tanstack-query/src/app/makeData.ts @@ -0,0 +1,48 @@ +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 +} + +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], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/with-tanstack-query/src/index.html b/examples/angular/with-tanstack-query/src/index.html new file mode 100644 index 0000000000..1b3f817b9e --- /dev/null +++ b/examples/angular/with-tanstack-query/src/index.html @@ -0,0 +1,13 @@ + + + + + Basic + + + + + + + + diff --git a/examples/angular/with-tanstack-query/src/main.ts b/examples/angular/with-tanstack-query/src/main.ts new file mode 100644 index 0000000000..8192dca694 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { App } from './app/app' + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/with-tanstack-query/src/styles.css b/examples/angular/with-tanstack-query/src/styles.css new file mode 100644 index 0000000000..3546a66331 --- /dev/null +++ b/examples/angular/with-tanstack-query/src/styles.css @@ -0,0 +1,371 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +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; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/angular/with-tanstack-query/tsconfig.app.json b/examples/angular/with-tanstack-query/tsconfig.app.json new file mode 100644 index 0000000000..a0dcc37c60 --- /dev/null +++ b/examples/angular/with-tanstack-query/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/examples/angular/with-tanstack-query/tsconfig.json b/examples/angular/with-tanstack-query/tsconfig.json new file mode 100644 index 0000000000..32789cdc2b --- /dev/null +++ b/examples/angular/with-tanstack-query/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "include": ["**/*.ts", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/lit/column-resizing/src/main.ts b/examples/lit/column-resizing/src/main.ts index 8236c61756..7c99238d1d 100644 --- a/examples/lit/column-resizing/src/main.ts +++ b/examples/lit/column-resizing/src/main.ts @@ -112,7 +112,7 @@ class LitTableExample extends LitElement { >[number]['headers'][number], ) => { if (this.columnResizeMode === 'onEnd' && header.column.getIsResizing()) { - const delta = table.store.state.columnResizing.deltaOffset ?? 0 + const delta = table.state.columnResizing.deltaOffset ?? 0 const dir = this.columnResizeDirection === 'rtl' ? -1 : 1 return `translateX(${dir * delta}px)` } diff --git a/examples/preact/basic-external-atoms/package.json b/examples/preact/basic-external-atoms/package.json index bf3417d967..6f752240cc 100644 --- a/examples/preact/basic-external-atoms/package.json +++ b/examples/preact/basic-external-atoms/package.json @@ -12,8 +12,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-store": "^0.13.1", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { diff --git a/examples/preact/basic-external-atoms/src/main.tsx b/examples/preact/basic-external-atoms/src/main.tsx index 5ed17a896a..e60f3b17fc 100644 --- a/examples/preact/basic-external-atoms/src/main.tsx +++ b/examples/preact/basic-external-atoms/src/main.tsx @@ -1,4 +1,5 @@ import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' import { useReducer, useState } from 'preact/hooks' import './index.css' import { useCreateAtom, useSelector } from '@tanstack/preact-store' @@ -12,6 +13,10 @@ import { tableFeatures, useTable, } from '@tanstack/preact-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' import { makeData } from './makeData' import type { PaginationState, SortingState } from '@tanstack/preact-table' @@ -76,6 +81,7 @@ function App() { // Create the table and pass your per-slice external atoms. const table = useTable( { + key: 'basic-external-atoms', // needed for devtools _features, _rowModels: { sortedRowModel: createSortedRowModel(sortFns), @@ -92,6 +98,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + return (
@@ -213,4 +221,10 @@ function App() { const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') -render(, rootElement) +render( + <> + + + , + rootElement, +) diff --git a/examples/preact/basic-external-state/package.json b/examples/preact/basic-external-state/package.json index 410995dc5f..1ff6e9b16a 100644 --- a/examples/preact/basic-external-state/package.json +++ b/examples/preact/basic-external-state/package.json @@ -12,8 +12,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-store": "^0.13.1", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { diff --git a/examples/preact/basic-external-state/src/main.tsx b/examples/preact/basic-external-state/src/main.tsx index 9057e5a3d5..06d306b758 100644 --- a/examples/preact/basic-external-state/src/main.tsx +++ b/examples/preact/basic-external-state/src/main.tsx @@ -1,4 +1,5 @@ import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' import { useReducer, useState } from 'preact/hooks' import './index.css' import { @@ -11,6 +12,10 @@ import { tableFeatures, useTable, } from '@tanstack/preact-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' import { makeData } from './makeData' import type { PaginationState, SortingState } from '@tanstack/preact-table' @@ -75,6 +80,7 @@ function App() { // Create the table and pass state + onChange handlers const table = useTable( { + key: 'basic-external-state', // needed for devtools debugTable: true, _features, _rowModels: { @@ -93,6 +99,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + return (
@@ -214,4 +222,10 @@ function App() { const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') -render(, rootElement) +render( + <> + + + , + rootElement, +) diff --git a/examples/preact/basic-subscribe/package.json b/examples/preact/basic-subscribe/package.json index 0ced2a8b82..af2edfc5dd 100644 --- a/examples/preact/basic-subscribe/package.json +++ b/examples/preact/basic-subscribe/package.json @@ -12,14 +12,14 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-store": "^0.13.1", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { "@preact/preset-vite": "^2.10.5", - "@tanstack/preact-devtools": "^0.10.5", - "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "typescript": "6.0.3", "vite": "^8.0.13" } diff --git a/examples/preact/basic-subscribe/src/main.tsx b/examples/preact/basic-subscribe/src/main.tsx index 167ad3b406..342d99bd65 100644 --- a/examples/preact/basic-subscribe/src/main.tsx +++ b/examples/preact/basic-subscribe/src/main.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks' import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' import { Subscribe, columnFilteringFeature, @@ -17,7 +18,6 @@ import { tableDevtoolsPlugin, useTanStackTableDevtools, } from '@tanstack/preact-table-devtools' -import { TanStackDevtools } from '@tanstack/preact-devtools' import { useCreateAtom } from '@tanstack/preact-store' import { makeData } from './makeData' import type { @@ -134,6 +134,7 @@ function App() { const table = useTable( { + key: 'basic-subscribe', // needed for devtools _features, _rowModels: { filteredRowModel: createFilteredRowModel(filterFns), @@ -152,7 +153,7 @@ function App() { () => null, // subscribe to no table state by default; use table.Subscribe below for targeted updates ) - useTanStackTableDevtools(table, 'Basic Subscribe Example') + useTanStackTableDevtools(table) return (
@@ -472,7 +473,6 @@ if (!rootElement) throw new Error('Failed to find the root element') render( <> - , rootElement, ) diff --git a/examples/preact/basic-use-app-table/package.json b/examples/preact/basic-use-app-table/package.json index 60f351702b..47a64351d2 100644 --- a/examples/preact/basic-use-app-table/package.json +++ b/examples/preact/basic-use-app-table/package.json @@ -11,7 +11,9 @@ "test:types": "tsc" }, "dependencies": { + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { diff --git a/examples/preact/basic-use-app-table/src/main.tsx b/examples/preact/basic-use-app-table/src/main.tsx index 4676e181b7..b98f94e6e3 100644 --- a/examples/preact/basic-use-app-table/src/main.tsx +++ b/examples/preact/basic-use-app-table/src/main.tsx @@ -1,6 +1,11 @@ import { useReducer, useState } from 'preact/hooks' import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' import { createTableHook } from '@tanstack/preact-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' import './index.css' // 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. @@ -105,6 +110,7 @@ function App() { // Features and row models are already defined in the createTableHook call above const table = useAppTable( { + key: 'basic-use-app-table', // needed for devtools debugTable: true, columns, data, @@ -113,6 +119,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + // 8. Render your table markup from the table instance APIs return (
@@ -166,4 +174,10 @@ function App() { const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') -render(, rootElement) +render( + <> + + + , + rootElement, +) diff --git a/examples/preact/basic-use-table/package.json b/examples/preact/basic-use-table/package.json index 9708e31d9f..37c7f9f6a7 100644 --- a/examples/preact/basic-use-table/package.json +++ b/examples/preact/basic-use-table/package.json @@ -11,7 +11,9 @@ "test:types": "tsc" }, "dependencies": { + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { diff --git a/examples/preact/basic-use-table/src/main.tsx b/examples/preact/basic-use-table/src/main.tsx index 9ac3c50d32..15d1a7b359 100644 --- a/examples/preact/basic-use-table/src/main.tsx +++ b/examples/preact/basic-use-table/src/main.tsx @@ -1,6 +1,11 @@ import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' import { useReducer, useState } from 'preact/hooks' import { tableFeatures, useTable } from '@tanstack/preact-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' import type { ColumnDef } from '@tanstack/preact-table' import './index.css' @@ -98,6 +103,7 @@ function App() { // 6. Create the table instance with required _features, columns, and data const table = useTable( { + key: 'basic-use-table', // needed for devtools debugTable: true, _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 @@ -108,6 +114,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + // 7. Render your table markup from the table instance APIs return (
@@ -161,4 +169,10 @@ function App() { const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') -render(, rootElement) +render( + <> + + + , + rootElement, +) diff --git a/examples/preact/column-resizing-performant/src/main.tsx b/examples/preact/column-resizing-performant/src/main.tsx index 4993831758..5734c6b0ff 100644 --- a/examples/preact/column-resizing-performant/src/main.tsx +++ b/examples/preact/column-resizing-performant/src/main.tsx @@ -178,7 +178,7 @@ function App() { ))}
{/* When resizing any column we will render this special memoized version of our table body */} - {table.store.state.columnResizing.isResizingColumn && enableMemo ? ( + {table.state.columnResizing.isResizingColumn && enableMemo ? ( ) : ( diff --git a/examples/preact/column-resizing/src/main.tsx b/examples/preact/column-resizing/src/main.tsx index 8b1c60fe68..48efc9fbbe 100644 --- a/examples/preact/column-resizing/src/main.tsx +++ b/examples/preact/column-resizing/src/main.tsx @@ -157,8 +157,7 @@ function App() { (table.options.columnResizeDirection === 'rtl' ? -1 : 1) * - (table.store.state.columnResizing - .deltaOffset ?? 0) + (table.state.columnResizing.deltaOffset ?? 0) }px)` : '', }} @@ -219,8 +218,7 @@ function App() { (table.options.columnResizeDirection === 'rtl' ? -1 : 1) * - (table.store.state.columnResizing - .deltaOffset ?? 0) + (table.state.columnResizing.deltaOffset ?? 0) }px)` : '', }} @@ -295,8 +293,7 @@ function App() { (table.options.columnResizeDirection === 'rtl' ? -1 : 1) * - (table.store.state.columnResizing - .deltaOffset ?? 0) + (table.state.columnResizing.deltaOffset ?? 0) }px)` : '', }} diff --git a/examples/preact/column-sizing/src/main.tsx b/examples/preact/column-sizing/src/main.tsx index cae3109dcc..b3f04851fc 100644 --- a/examples/preact/column-sizing/src/main.tsx +++ b/examples/preact/column-sizing/src/main.tsx @@ -94,7 +94,7 @@ function App() { // Don't actually do this to resize columns. This is just for demonstration purposes. // See the Column Resizing Example for how to do this with dedicated resizing APIs efficiently. table.setColumnSizing({ - ...table.store.state.columnSizing, + ...table.state.columnSizing, [column.id]: Number((e.target as HTMLInputElement).value), }) }} diff --git a/examples/preact/composable-tables/package.json b/examples/preact/composable-tables/package.json index 92279e9862..832e8fa5d9 100644 --- a/examples/preact/composable-tables/package.json +++ b/examples/preact/composable-tables/package.json @@ -12,8 +12,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-store": "^0.13.1", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { diff --git a/examples/preact/composable-tables/src/main.tsx b/examples/preact/composable-tables/src/main.tsx index 473ca140e7..2d7b171b66 100644 --- a/examples/preact/composable-tables/src/main.tsx +++ b/examples/preact/composable-tables/src/main.tsx @@ -1,5 +1,10 @@ import { useCallback, useMemo, useState } from 'preact/hooks' import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' import { createAppColumnHelper, useAppTable } from './hooks/table' import { makeData, makeProductData } from './makeData' import type { Person, Product } from './makeData' @@ -71,6 +76,7 @@ function UsersTable() { // Create the table - _features and _rowModels are already configured! const table = useAppTable( { + key: 'users-table', // needed for devtools columns, data, debugTable: true, @@ -79,6 +85,8 @@ function UsersTable() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + return ( // Main selector on AppTable - selects all needed state in one place state, // default selector ) + useTanStackTableDevtools(table) + return ( ({ @@ -432,4 +443,10 @@ function App() { const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') -render(, rootElement) +render( + <> + + + , + rootElement, +) diff --git a/examples/preact/custom-plugin/src/main.tsx b/examples/preact/custom-plugin/src/main.tsx index d8d1119060..dfd53b4021 100644 --- a/examples/preact/custom-plugin/src/main.tsx +++ b/examples/preact/custom-plugin/src/main.tsx @@ -288,7 +288,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -296,7 +296,7 @@ function App() { | Go to page: { const page = (e.target as HTMLInputElement).value ? Number((e.target as HTMLInputElement).value) - 1 @@ -307,7 +307,7 @@ function App() { /> { table.setPageSize(Number((e.target as HTMLSelectElement).value)) }} diff --git a/examples/preact/row-selection/package.json b/examples/preact/row-selection/package.json index 06d1e5474a..99223cb6e3 100644 --- a/examples/preact/row-selection/package.json +++ b/examples/preact/row-selection/package.json @@ -12,14 +12,14 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/preact-devtools": "^0.10.5", "@tanstack/preact-store": "^0.13.1", "@tanstack/preact-table": "^9.0.0-alpha.49", + "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "preact": "^10.29.2" }, "devDependencies": { "@preact/preset-vite": "^2.10.5", - "@tanstack/preact-devtools": "^0.10.5", - "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "typescript": "6.0.3", "vite": "^8.0.13" } diff --git a/examples/preact/row-selection/src/main.tsx b/examples/preact/row-selection/src/main.tsx index 96fe2eb68f..3376e58c2d 100644 --- a/examples/preact/row-selection/src/main.tsx +++ b/examples/preact/row-selection/src/main.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks' import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' import { columnFilteringFeature, createColumnHelper, @@ -16,7 +17,6 @@ import { tableDevtoolsPlugin, useTanStackTableDevtools, } from '@tanstack/preact-table-devtools' -import { TanStackDevtools } from '@tanstack/preact-devtools' import { useCreateAtom } from '@tanstack/preact-store' import { makeData } from './makeData' import type { @@ -105,6 +105,7 @@ function App() { const table = useTable( { + key: 'row-selection', // needed for devtools _features, _rowModels: { filteredRowModel: createFilteredRowModel(filterFns), @@ -123,7 +124,7 @@ function App() { (state) => state, // default selector ) - useTanStackTableDevtools(table, 'Row Selection Example') + useTanStackTableDevtools(table) return ( <> @@ -394,7 +395,6 @@ if (!rootElement) throw new Error('Failed to find the root element') render( <> - , rootElement, ) diff --git a/examples/preact/sorting/package.json b/examples/preact/sorting/package.json index c6df5c2ab3..6baae65d79 100644 --- a/examples/preact/sorting/package.json +++ b/examples/preact/sorting/package.json @@ -17,8 +17,6 @@ }, "devDependencies": { "@preact/preset-vite": "^2.10.5", - "@tanstack/preact-devtools": "^0.10.5", - "@tanstack/preact-table-devtools": "^9.0.0-alpha.49", "typescript": "6.0.3", "vite": "^8.0.13" } diff --git a/examples/preact/sorting/src/main.tsx b/examples/preact/sorting/src/main.tsx index 94572ef99e..69d4e1566b 100644 --- a/examples/preact/sorting/src/main.tsx +++ b/examples/preact/sorting/src/main.tsx @@ -1,11 +1,5 @@ import { render } from 'preact' import { useMemo, useReducer, useState } from 'preact/hooks' -import './index.css' -import { TanStackDevtools } from '@tanstack/preact-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/preact-table-devtools' import { createColumnHelper, createSortedRowModel, @@ -105,8 +99,6 @@ function App() { (state) => state, // default selector ) - useTanStackTableDevtools(table, 'Sorting Example') - return (
@@ -187,7 +179,6 @@ if (!rootElement) throw new Error('Failed to find the root element') render( <> - , rootElement, ) diff --git a/examples/react/basic-external-atoms/package.json b/examples/react/basic-external-atoms/package.json index 8db0915829..c442eee0aa 100644 --- a/examples/react/basic-external-atoms/package.json +++ b/examples/react/basic-external-atoms/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/examples/react/basic-external-atoms/src/main.tsx b/examples/react/basic-external-atoms/src/main.tsx index f22eef25ae..a2c5a7f8bc 100644 --- a/examples/react/basic-external-atoms/src/main.tsx +++ b/examples/react/basic-external-atoms/src/main.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import ReactDOM from 'react-dom/client' import './index.css' import { useCreateAtom, useSelector } from '@tanstack/react-store' @@ -12,6 +13,10 @@ import { tableFeatures, useTable, } from '@tanstack/react-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' import { makeData } from './makeData' import type { PaginationState, SortingState } from '@tanstack/react-table' @@ -79,6 +84,7 @@ function App() { // Create the table and pass your per-slice external atoms. const table = useTable( { + key: 'basic-external-atoms', // needed for devtools _features, _rowModels: { sortedRowModel: createSortedRowModel(sortFns), @@ -95,6 +101,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + return (
@@ -231,5 +239,6 @@ if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( + , ) diff --git a/examples/react/basic-external-state/package.json b/examples/react/basic-external-state/package.json index e17b185e01..eeaebe1afe 100644 --- a/examples/react/basic-external-state/package.json +++ b/examples/react/basic-external-state/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/examples/react/basic-external-state/src/main.tsx b/examples/react/basic-external-state/src/main.tsx index 031c3f290b..9f24d56907 100644 --- a/examples/react/basic-external-state/src/main.tsx +++ b/examples/react/basic-external-state/src/main.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import ReactDOM from 'react-dom/client' import './index.css' import { @@ -11,6 +12,10 @@ import { tableFeatures, useTable, } from '@tanstack/react-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' import { makeData } from './makeData' import type { PaginationState, SortingState } from '@tanstack/react-table' @@ -75,6 +80,7 @@ function App() { // Create the table and pass state + onChange handlers const table = useTable( { + key: 'basic-external-state', // needed for devtools debugTable: true, _features, _rowModels: { @@ -93,6 +99,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + return (
@@ -220,5 +228,6 @@ if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( + , ) diff --git a/examples/react/basic-subscribe/package.json b/examples/react/basic-subscribe/package.json index 5f95519278..02f1359f3a 100644 --- a/examples/react/basic-subscribe/package.json +++ b/examples/react/basic-subscribe/package.json @@ -11,17 +11,17 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-pacer": "^0.22.1", "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, "devDependencies": { "@rolldown/plugin-babel": "^0.2.3", "@rollup/plugin-replace": "^6.0.3", - "@tanstack/react-devtools": "^0.10.5", - "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.2", diff --git a/examples/react/basic-subscribe/src/main.tsx b/examples/react/basic-subscribe/src/main.tsx index f116c0a215..44c3fd57f9 100644 --- a/examples/react/basic-subscribe/src/main.tsx +++ b/examples/react/basic-subscribe/src/main.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import ReactDOM from 'react-dom/client' import { Subscribe, @@ -13,12 +14,11 @@ import { tableFeatures, useTable, } from '@tanstack/react-table' -import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' import { tableDevtoolsPlugin, useTanStackTableDevtools, } from '@tanstack/react-table-devtools' -import { TanStackDevtools } from '@tanstack/react-devtools' +import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' import { useCreateAtom } from '@tanstack/react-store' import { makeData } from './makeData' import type { HTMLProps } from 'react' @@ -135,6 +135,7 @@ function App() { const table = useTable( { + key: 'basic-subscribe', // needed for devtools _features, _rowModels: { filteredRowModel: createFilteredRowModel(filterFns), @@ -153,7 +154,7 @@ function App() { () => null, // subscribe to no table state by default; use table.Subscribe below for targeted updates ) - useTanStackTableDevtools(table, 'Basic Subscribe Example') + useTanStackTableDevtools(table) return (
diff --git a/examples/react/basic-use-app-table/package.json b/examples/react/basic-use-app-table/package.json index a854127950..9e8b0bdc58 100644 --- a/examples/react/basic-use-app-table/package.json +++ b/examples/react/basic-use-app-table/package.json @@ -10,7 +10,9 @@ "test:types": "tsc" }, "dependencies": { + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/examples/react/basic-use-app-table/src/main.tsx b/examples/react/basic-use-app-table/src/main.tsx index e00d6c98eb..37c9416559 100644 --- a/examples/react/basic-use-app-table/src/main.tsx +++ b/examples/react/basic-use-app-table/src/main.tsx @@ -1,6 +1,11 @@ import * as React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import ReactDOM from 'react-dom/client' import { createTableHook } from '@tanstack/react-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' import './index.css' // 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. @@ -105,6 +110,7 @@ function App() { // Features and row models are already defined in the createTableHook call above const table = useAppTable( { + key: 'basic-use-app-table', // needed for devtools debugTable: true, columns, data, @@ -113,6 +119,8 @@ function App() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + // 8. Render your table markup from the table instance APIs return (
@@ -169,5 +177,6 @@ if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( + , ) diff --git a/examples/react/basic-use-legacy-table/package.json b/examples/react/basic-use-legacy-table/package.json index d5cabc397c..d19808b612 100644 --- a/examples/react/basic-use-legacy-table/package.json +++ b/examples/react/basic-use-legacy-table/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-pacer": "^0.22.1", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/examples/react/basic-use-legacy-table/src/main.tsx b/examples/react/basic-use-legacy-table/src/main.tsx index c01b16e12b..88720b6e40 100644 --- a/examples/react/basic-use-legacy-table/src/main.tsx +++ b/examples/react/basic-use-legacy-table/src/main.tsx @@ -11,9 +11,14 @@ * New code should use useTable with explicit _features and _rowModels. */ import React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import ReactDOM from 'react-dom/client' import './index.css' import { flexRender } from '@tanstack/react-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' import { getCoreRowModel, @@ -111,6 +116,7 @@ function App() { // Notice how we use get*RowModel() options instead of _rowModels // and we don't need to define _features const table = useLegacyTable({ + key: 'basic-use-legacy-table', // needed for devtools columns, data, // V8-style row model options (these are mapped to v9 _rowModels under the hood) @@ -131,6 +137,8 @@ function App() { debugColumns: true, }) + useTanStackTableDevtools(table) + return (
@@ -377,5 +385,6 @@ if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( + , ) diff --git a/examples/react/basic-use-table/package.json b/examples/react/basic-use-table/package.json index 96ca3be01b..28ed7f3eda 100644 --- a/examples/react/basic-use-table/package.json +++ b/examples/react/basic-use-table/package.json @@ -10,7 +10,9 @@ "test:types": "tsc" }, "dependencies": { + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/examples/react/basic-use-table/src/main.tsx b/examples/react/basic-use-table/src/main.tsx index 5d60ad4d8f..f0649844a2 100644 --- a/examples/react/basic-use-table/src/main.tsx +++ b/examples/react/basic-use-table/src/main.tsx @@ -1,6 +1,11 @@ import * as React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import ReactDOM from 'react-dom/client' import { tableFeatures, useTable } from '@tanstack/react-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' import type { ColumnDef } from '@tanstack/react-table' import './index.css' @@ -98,7 +103,8 @@ function App() { // 6. Create the table instance with required _features, columns, and data const table = useTable( { - debugTable: true, + key: 'basic-use-table', // needed for devtools, omit if you don't want to use the devtools + debugTable: true, // optionally, enable console logging debug messages _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 columns, @@ -108,6 +114,9 @@ function App() { (state) => state, // default selector ) + // optionally, add this table instance to the devtools + useTanStackTableDevtools(table) + // 7. Render your table markup from the table instance APIs return (
@@ -164,5 +173,6 @@ if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( + , ) diff --git a/examples/react/column-dnd/src/main.tsx b/examples/react/column-dnd/src/main.tsx index cf28422cdf..ddd250cadd 100644 --- a/examples/react/column-dnd/src/main.tsx +++ b/examples/react/column-dnd/src/main.tsx @@ -197,7 +197,7 @@ function App() { {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( @@ -213,7 +213,7 @@ function App() { {row.getAllCells().map((cell) => ( diff --git a/examples/react/column-resizing-performant/src/main.tsx b/examples/react/column-resizing-performant/src/main.tsx index 3f9692ab4e..7b577245a4 100644 --- a/examples/react/column-resizing-performant/src/main.tsx +++ b/examples/react/column-resizing-performant/src/main.tsx @@ -181,7 +181,7 @@ function App() { ))}
{/* When resizing any column we will render this special memoized version of our table body */} - {table.store.state.columnResizing.isResizingColumn && enableMemo ? ( + {table.state.columnResizing.isResizingColumn && enableMemo ? ( ) : ( diff --git a/examples/react/column-resizing/src/main.tsx b/examples/react/column-resizing/src/main.tsx index dfa2822178..f375ca8234 100644 --- a/examples/react/column-resizing/src/main.tsx +++ b/examples/react/column-resizing/src/main.tsx @@ -158,8 +158,7 @@ function App() { (table.options.columnResizeDirection === 'rtl' ? -1 : 1) * - (table.store.state.columnResizing - .deltaOffset ?? 0) + (table.state.columnResizing.deltaOffset ?? 0) }px)` : '', }} @@ -220,8 +219,7 @@ function App() { (table.options.columnResizeDirection === 'rtl' ? -1 : 1) * - (table.store.state.columnResizing - .deltaOffset ?? 0) + (table.state.columnResizing.deltaOffset ?? 0) }px)` : '', }} @@ -296,8 +294,7 @@ function App() { (table.options.columnResizeDirection === 'rtl' ? -1 : 1) * - (table.store.state.columnResizing - .deltaOffset ?? 0) + (table.state.columnResizing.deltaOffset ?? 0) }px)` : '', }} diff --git a/examples/react/column-sizing/src/main.tsx b/examples/react/column-sizing/src/main.tsx index d41d3ce5f8..afa1d3a0fc 100644 --- a/examples/react/column-sizing/src/main.tsx +++ b/examples/react/column-sizing/src/main.tsx @@ -99,7 +99,7 @@ function App() { // Don't actually do this to resize columns. This is just for demonstration purposes. // See the Column Resizing Example for how to do this with dedicated resizing APIs efficiently. table.setColumnSizing({ - ...table.store.state.columnSizing, + ...table.state.columnSizing, [column.id]: Number(e.target.value), }) }} diff --git a/examples/react/composable-tables/package.json b/examples/react/composable-tables/package.json index 4302602f9c..bed2a62e8d 100644 --- a/examples/react/composable-tables/package.json +++ b/examples/react/composable-tables/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^9.0.0-alpha.49", + "@tanstack/react-table-devtools": "^9.0.0-alpha.49", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/examples/react/composable-tables/src/main.tsx b/examples/react/composable-tables/src/main.tsx index 63239f86f5..2fe06564c5 100644 --- a/examples/react/composable-tables/src/main.tsx +++ b/examples/react/composable-tables/src/main.tsx @@ -1,10 +1,14 @@ import * as React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' import { useCallback, useMemo, useState } from 'react' import ReactDOM from 'react-dom/client' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' import { createAppColumnHelper, useAppTable } from './hooks/table' import { makeData, makeProductData } from './makeData' import type { Person, Product } from './makeData' -import './index.css' // Import cell components directly - they use useCellContext internally // Create column helpers with TFeatures already bound - only need TData! @@ -72,6 +76,7 @@ function UsersTable() { // Create the table - _features and _rowModels are already configured! const table = useAppTable( { + key: 'users-table', // needed for devtools columns, data, debugTable: true, @@ -80,6 +85,8 @@ function UsersTable() { (state) => state, // default selector ) + useTanStackTableDevtools(table) + return ( // Main selector on AppTable - selects all needed state in one place state, // default selector ) + useTanStackTableDevtools(table) + return ( ({ @@ -436,5 +446,6 @@ if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( + , ) diff --git a/examples/react/custom-plugin/src/main.tsx b/examples/react/custom-plugin/src/main.tsx index cb18a3693f..d6d2a66c09 100644 --- a/examples/react/custom-plugin/src/main.tsx +++ b/examples/react/custom-plugin/src/main.tsx @@ -293,7 +293,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -301,7 +301,7 @@ function App() { | Go to page: { const page = e.target.value ? Number(e.target.value) - 1 : 0 table.setPageIndex(page) @@ -310,7 +310,7 @@ function App() { /> { table.setPageSize(Number(value)) }} > - + {pageSizeOptions.map((pageSize) => ( @@ -57,8 +55,8 @@ export function DataTablePagination({
- Page {(table.store.state.pagination.pageIndex + 1).toLocaleString()}{' '} - of {table.getPageCount().toLocaleString()} + Page {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()}
) } diff --git a/examples/solid/filters-faceted/src/App.tsx b/examples/solid/filters-faceted/src/App.tsx index bd42b0d89b..c887496f8f 100644 --- a/examples/solid/filters-faceted/src/App.tsx +++ b/examples/solid/filters-faceted/src/App.tsx @@ -116,7 +116,7 @@ function App() {
globalFilterDebouncer.maybeExecute(e.currentTarget.value) } @@ -200,7 +200,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state().pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -210,7 +210,7 @@ function App() { type="number" min="1" max={table.getPageCount()} - value={table.store.state.pagination.pageIndex + 1} + value={table.state().pagination.pageIndex + 1} onInput={(e) => { const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 @@ -221,7 +221,7 @@ function App() { /> { const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 @@ -240,7 +240,7 @@ function App() { /> globalFilterDebouncer.maybeExecute(e.currentTarget.value) } @@ -203,7 +203,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state().pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -213,7 +213,7 @@ function App() { type="number" min="1" max={table.getPageCount()} - value={table.store.state.pagination.pageIndex + 1} + value={table.state().pagination.pageIndex + 1} onInput={(e) => { const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 @@ -224,7 +224,7 @@ function App() { /> table.setPageSize(Number(e.currentTarget.value))} > @@ -234,7 +234,7 @@ function App() {
{table.getRowModel().rows.length.toLocaleString()} Rows
-
{JSON.stringify(table.store.state, null, 2)}
+
{JSON.stringify(table.state(), null, 2)}
) } diff --git a/examples/solid/kitchen-sink/package.json b/examples/solid/kitchen-sink/package.json index 2926929e3e..5dcc26c459 100644 --- a/examples/solid/kitchen-sink/package.json +++ b/examples/solid/kitchen-sink/package.json @@ -17,7 +17,9 @@ }, "dependencies": { "@tanstack/match-sorter-utils": "^9.0.0-alpha.4", + "@tanstack/solid-devtools": "^0.8.5", "@tanstack/solid-table": "^9.0.0-alpha.49", + "@tanstack/solid-table-devtools": "^9.0.0-alpha.49", "solid-js": "^1.9.13" } } diff --git a/examples/solid/kitchen-sink/src/App.tsx b/examples/solid/kitchen-sink/src/App.tsx index 80ca9c6381..6233bc407f 100644 --- a/examples/solid/kitchen-sink/src/App.tsx +++ b/examples/solid/kitchen-sink/src/App.tsx @@ -14,6 +14,7 @@ import { sortFns, stockFeatures, } from '@tanstack/solid-table' +import { useTanStackTableDevtools } from '@tanstack/solid-table-devtools' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' import { For, @@ -355,7 +356,7 @@ function TableCell(props: { table: AppTable }) { const className = () => { - const groupingActive = props.table.store.state.grouping.length > 0 + const groupingActive = props.table.state().grouping.length > 0 const hasAggregation = !!props.cell.column.columnDef.aggregationFn return !groupingActive ? undefined @@ -531,6 +532,7 @@ function App() { const table = createAppTable( { + key: 'kitchen-sink', // needed for devtools columns, get data() { return data() @@ -550,9 +552,11 @@ function App() { (state) => state, ) + useTanStackTableDevtools(table) + const columnSizeVars = createMemo(() => { - void table.store.state.columnResizing - void table.store.state.columnSizing + void table.state().columnResizing + void table.state().columnSizing const colSizes: Record = {} for (const header of table.getFlatHeaders()) { colSizes[`--header-${header.id}-size`] = header.getSize() @@ -577,7 +581,7 @@ function App() {
table.setGlobalFilter(String(value))} class="global-filter-input" placeholder="Fuzzy search all columns..." @@ -714,7 +718,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state().pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -724,7 +728,7 @@ function App() { type="number" min="1" max={table.getPageCount()} - value={table.store.state.pagination.pageIndex + 1} + value={table.state().pagination.pageIndex + 1} onInput={(e) => { const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 @@ -735,7 +739,7 @@ function App() { /> { table.setPageSize(Number(e.currentTarget.value)) }} @@ -181,7 +181,7 @@ function MyTable(props: { Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} {table.getRowCount().toLocaleString()} Rows
-
{JSON.stringify(table.store.state, null, 2)}
+
{JSON.stringify(table.state(), null, 2)}
) } diff --git a/examples/solid/row-pinning/src/App.tsx b/examples/solid/row-pinning/src/App.tsx index 128a4fb3de..62809a5c9b 100644 --- a/examples/solid/row-pinning/src/App.tsx +++ b/examples/solid/row-pinning/src/App.tsx @@ -250,7 +250,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state().pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -260,7 +260,7 @@ function App() { type="number" min="1" max={table.getPageCount()} - value={table.store.state.pagination.pageIndex + 1} + value={table.state().pagination.pageIndex + 1} onInput={(e) => { const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 @@ -271,7 +271,7 @@ function App() { /> table.setGlobalFilter(e.target.value)} class="summary-panel" placeholder="Search all columns..." @@ -233,7 +235,7 @@ function App() {
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state().pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -243,7 +245,7 @@ function App() { type="number" min="1" max={table.getPageCount()} - value={table.store.state.pagination.pageIndex + 1} + value={table.state().pagination.pageIndex + 1} onInput={(e) => { const page = e.target.value ? Number(e.target.value) - 1 : 0 table.setPageIndex(page) @@ -252,7 +254,7 @@ function App() { /> { table.setPageSize(Number(e.currentTarget.value)) }} diff --git a/examples/solid/with-tanstack-router/src/components/table.tsx b/examples/solid/with-tanstack-router/src/components/table.tsx index 471d9c545a..6c1f058b0b 100644 --- a/examples/solid/with-tanstack-router/src/components/table.tsx +++ b/examples/solid/with-tanstack-router/src/components/table.tsx @@ -195,7 +195,7 @@ export default function Table>(
Page
- {(table.store.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {(table.state().pagination.pageIndex + 1).toLocaleString()} of{' '} {table.getPageCount().toLocaleString()}
@@ -203,7 +203,7 @@ export default function Table>( | Go to page: { const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 @@ -214,7 +214,7 @@ export default function Table>( /> { table.setPageSize(Number(e.currentTarget.value)) }} diff --git a/examples/svelte/composable-tables/src/components/ProductsTable.svelte b/examples/svelte/composable-tables/src/components/ProductsTable.svelte index eba8efbdf6..596db98a56 100644 --- a/examples/svelte/composable-tables/src/components/ProductsTable.svelte +++ b/examples/svelte/composable-tables/src/components/ProductsTable.svelte @@ -62,14 +62,14 @@ }) // Reactive derived values from table state - let sorting = $derived(table.store.state.sorting) - let columnFilters = $derived(table.store.state.columnFilters) + let sorting = $derived(table.state.sorting) + let columnFilters = $derived(table.state.columnFilters) // IMPORTANT: Derive rows from table state so Svelte tracks the dependency. // We must read a $state value that changes on every table update. // JSON.stringify forces a deep read, ensuring Svelte sees the dependency. const rows = $derived.by(() => { - JSON.stringify(table.store.state) + JSON.stringify(table.state) return table.getRowModel().rows }) diff --git a/examples/svelte/composable-tables/src/components/UsersTable.svelte b/examples/svelte/composable-tables/src/components/UsersTable.svelte index ed85bff648..a83910e657 100644 --- a/examples/svelte/composable-tables/src/components/UsersTable.svelte +++ b/examples/svelte/composable-tables/src/components/UsersTable.svelte @@ -72,16 +72,16 @@ }) // Reactive derived values from table state. - // Reading table.store.state creates a $state dependency (via the notifier) + // Reading table.state creates a $state dependency (via the notifier) // that triggers re-renders when any table state changes. - let sorting = $derived(table.store.state.sorting) - let columnFilters = $derived(table.store.state.columnFilters) + let sorting = $derived(table.state.sorting) + let columnFilters = $derived(table.state.columnFilters) // IMPORTANT: Derive rows from table state so Svelte tracks the dependency. // We must read a $state value that changes on every table update. // JSON.stringify forces a deep read, ensuring Svelte sees the dependency. const rows = $derived.by(() => { - JSON.stringify(table.store.state) + JSON.stringify(table.state) return table.getRowModel().rows }) diff --git a/examples/svelte/filters-fuzzy/src/App.svelte b/examples/svelte/filters-fuzzy/src/App.svelte index 6034180890..e562313191 100644 --- a/examples/svelte/filters-fuzzy/src/App.svelte +++ b/examples/svelte/filters-fuzzy/src/App.svelte @@ -107,8 +107,8 @@ ) $effect(() => { - if (table.store.state.columnFilters[0]?.id === 'fullName') { - if (table.store.state.sorting[0]?.id !== 'fullName') { + if (table.state.columnFilters[0]?.id === 'fullName') { + if (table.state.sorting[0]?.id !== 'fullName') { table.setSorting([{ id: 'fullName', desc: false }]) } diff --git a/examples/vue/basic-external-atoms/package.json b/examples/vue/basic-external-atoms/package.json index e6ac03b0dc..dfa7ff9f6e 100644 --- a/examples/vue/basic-external-atoms/package.json +++ b/examples/vue/basic-external-atoms/package.json @@ -10,8 +10,10 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/vue-devtools": "^0.2.19", "@tanstack/vue-store": "^0.11.0", "@tanstack/vue-table": "^9.0.0-alpha.49", + "@tanstack/vue-table-devtools": "^9.0.0-alpha.49", "vue": "^3.5.34" }, "devDependencies": { diff --git a/examples/vue/basic-external-atoms/src/App.tsx b/examples/vue/basic-external-atoms/src/App.tsx index 77a65b2cd9..c45770f113 100644 --- a/examples/vue/basic-external-atoms/src/App.tsx +++ b/examples/vue/basic-external-atoms/src/App.tsx @@ -11,6 +11,7 @@ import { tableFeatures, useTable, } from '@tanstack/vue-table' +import { useTanStackTableDevtools } from '@tanstack/vue-table-devtools' import { makeData } from './makeData' import type { Cell, @@ -75,6 +76,7 @@ export default defineComponent({ const pagination = useSelector(paginationAtom) const table = useTable({ + key: 'basic-external-atoms', // needed for devtools _features, _rowModels: { sortedRowModel: createSortedRowModel(sortFns), @@ -91,6 +93,8 @@ export default defineComponent({ debugTable: true, }) + useTanStackTableDevtools(table) + return () => (
diff --git a/examples/vue/basic-external-atoms/src/main.ts b/examples/vue/basic-external-atoms/src/main.ts index 50a4dab0d5..a399341490 100644 --- a/examples/vue/basic-external-atoms/src/main.ts +++ b/examples/vue/basic-external-atoms/src/main.ts @@ -1,5 +1,18 @@ -import { createApp } from 'vue' +import { createApp, defineComponent, h } from 'vue' +import { TanStackDevtools } from '@tanstack/vue-devtools' +import { tableDevtoolsPlugin } from '@tanstack/vue-table-devtools' import App from './App.vue' import './index.css' -createApp(App).mount('#app') +const Root = defineComponent({ + setup() { + return () => [ + h(App), + h(TanStackDevtools, { + plugins: [tableDevtoolsPlugin({})], + }), + ] + }, +}) + +createApp(Root).mount('#app') diff --git a/examples/vue/basic-external-state/package.json b/examples/vue/basic-external-state/package.json index 8803c2e517..e094b709bd 100644 --- a/examples/vue/basic-external-state/package.json +++ b/examples/vue/basic-external-state/package.json @@ -10,7 +10,9 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/vue-devtools": "^0.2.19", "@tanstack/vue-table": "^9.0.0-alpha.49", + "@tanstack/vue-table-devtools": "^9.0.0-alpha.49", "vue": "^3.5.34" }, "devDependencies": { diff --git a/examples/vue/basic-external-state/src/App.tsx b/examples/vue/basic-external-state/src/App.tsx index 04c24679cd..665815cec3 100644 --- a/examples/vue/basic-external-state/src/App.tsx +++ b/examples/vue/basic-external-state/src/App.tsx @@ -10,6 +10,7 @@ import { tableFeatures, useTable, } from '@tanstack/vue-table' +import { useTanStackTableDevtools } from '@tanstack/vue-table-devtools' import { makeData } from './makeData' import type { Cell, @@ -79,6 +80,7 @@ export default defineComponent({ const table = useTable( { + key: 'basic-external-state', // needed for devtools debugTable: true, _features, _rowModels: { @@ -110,6 +112,8 @@ export default defineComponent({ }), ) + useTanStackTableDevtools(table) + return () => (
diff --git a/examples/vue/basic-external-state/src/main.ts b/examples/vue/basic-external-state/src/main.ts index 50a4dab0d5..a399341490 100644 --- a/examples/vue/basic-external-state/src/main.ts +++ b/examples/vue/basic-external-state/src/main.ts @@ -1,5 +1,18 @@ -import { createApp } from 'vue' +import { createApp, defineComponent, h } from 'vue' +import { TanStackDevtools } from '@tanstack/vue-devtools' +import { tableDevtoolsPlugin } from '@tanstack/vue-table-devtools' import App from './App.vue' import './index.css' -createApp(App).mount('#app') +const Root = defineComponent({ + setup() { + return () => [ + h(App), + h(TanStackDevtools, { + plugins: [tableDevtoolsPlugin({})], + }), + ] + }, +}) + +createApp(Root).mount('#app') diff --git a/examples/vue/basic-use-app-table/package.json b/examples/vue/basic-use-app-table/package.json index 4d8fcb11a5..5f7d5123ac 100644 --- a/examples/vue/basic-use-app-table/package.json +++ b/examples/vue/basic-use-app-table/package.json @@ -9,7 +9,9 @@ "test:types": "vue-tsc" }, "dependencies": { + "@tanstack/vue-devtools": "^0.2.19", "@tanstack/vue-table": "^9.0.0-alpha.49", + "@tanstack/vue-table-devtools": "^9.0.0-alpha.49", "vue": "^3.5.34" }, "devDependencies": { diff --git a/examples/vue/basic-use-app-table/src/App.vue b/examples/vue/basic-use-app-table/src/App.vue index e01be6f776..fd89b0155a 100644 --- a/examples/vue/basic-use-app-table/src/App.vue +++ b/examples/vue/basic-use-app-table/src/App.vue @@ -1,4 +1,5 @@