@tanstack/angular-table 9.0.0-beta.38 → 9.0.0-beta.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +1 -0
  2. package/dist/README.md +1 -0
  3. package/package.json +2 -2
  4. package/skills/create-table-hook/SKILL.md +148 -0
  5. package/skills/getting-started/SKILL.md +138 -0
  6. package/skills/migrate-v8-to-v9/SKILL.md +194 -0
  7. package/skills/table-state/SKILL.md +197 -0
  8. package/skills/with-tanstack-query/SKILL.md +142 -0
  9. package/skills/with-tanstack-virtual/SKILL.md +126 -0
  10. package/skills/angular/angular-rendering-directives/SKILL.md +0 -415
  11. package/skills/angular/angular-rendering-directives/references/content-shapes.md +0 -142
  12. package/skills/angular/angular-rendering-directives/references/create-table-hook-registries.md +0 -89
  13. package/skills/angular/angular-rendering-directives/references/di-tokens.md +0 -171
  14. package/skills/angular/angular-rendering-directives/references/flex-render-component-options.md +0 -64
  15. package/skills/angular/client-to-server/SKILL.md +0 -468
  16. package/skills/angular/compose-with-tanstack-query/SKILL.md +0 -486
  17. package/skills/angular/compose-with-tanstack-store/SKILL.md +0 -405
  18. package/skills/angular/compose-with-tanstack-virtual/SKILL.md +0 -399
  19. package/skills/angular/getting-started/SKILL.md +0 -487
  20. package/skills/angular/getting-started/references/feature-row-model-mapping.md +0 -51
  21. package/skills/angular/migrate-v8-to-v9/SKILL.md +0 -422
  22. package/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md +0 -268
  23. package/skills/angular/production-readiness/SKILL.md +0 -472
  24. package/skills/angular/table-state/SKILL.md +0 -422
  25. package/skills/angular/table-state/references/external-atoms-and-app-hook.md +0 -153
@@ -1,171 +0,0 @@
1
- # DI Tokens & Context Injection — Full Reference
2
-
3
- `FlexRender` automatically provides DI tokens for the render context to any
4
- component rendered through `*flexRender` / `*flexRenderCell` / etc. The renderer
5
- inspects the props object — if it has `table`, `header`, or `cell`, it
6
- provides the matching token in the child injector.
7
-
8
- | Helper | Returns | Available inside |
9
- | ------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ |
10
- | `injectFlexRenderContext<T>()` | `T` (full props proxy) | a component rendered via `*flexRender` / `*flexRenderCell` / `*flexRenderHeader` / `*flexRenderFooter` |
11
- | `injectTableContext<TData>()` | `Signal<Table<TFeatures, TData>>` | anything under `[tanStackTable]` or rendered via `*flexRender` whose context has `table` |
12
- | `injectTableHeaderContext<TValue, TData>()` | `Signal<Header<...>>` | anything under `[tanStackTableHeader]` or rendered via `*flexRenderHeader` |
13
- | `injectTableCellContext<TValue, TData>()` | `Signal<Cell<...>>` | anything under `[tanStackTableCell]` or rendered via `*flexRenderCell` |
14
-
15
- The full props passed via `*flexRender`'s `props:` (or auto-derived by the
16
- shorthand directives) is wrapped in a `Proxy` so property access always reads
17
- the _latest_ value across re-renders.
18
-
19
- ---
20
-
21
- ## Pattern A — inject inside the rendered component itself
22
-
23
- If your component **is** the cell, header, or footer (rendered through
24
- `*flexRender*`), you don't need any extra directive — the token is auto-provided.
25
-
26
- ```ts
27
- @Component({
28
- template: `
29
- <span>{{ context.getValue() }}</span>
30
- <button (click)="context.row.toggleSelected()">Toggle</button>
31
- `,
32
- })
33
- export class InteractiveCell {
34
- readonly context = injectFlexRenderContext<CellContext<any, any, any>>()
35
- }
36
- ```
37
-
38
- Or, for just the cell signal:
39
-
40
- ```ts
41
- @Component({ template: `{{ cell().getValue() }}` })
42
- export class TextCell {
43
- readonly cell = injectTableCellContext<string>()
44
- }
45
- ```
46
-
47
- ---
48
-
49
- ## Pattern B — context for _descendants outside the FlexRender tree_
50
-
51
- When you have a component that lives next to the `*flexRenderCell` block, or
52
- when you want any nested element to inject the table/header/cell, use the
53
- context **host directives**:
54
-
55
- ```ts
56
- import {
57
- FlexRender,
58
- TanStackTable,
59
- TanStackTableHeader,
60
- TanStackTableCell,
61
- } from '@tanstack/angular-table'
62
-
63
- @Component({
64
- imports: [FlexRender, TanStackTable, TanStackTableHeader, TanStackTableCell],
65
- templateUrl: './app.html',
66
- })
67
- export class App {}
68
- ```
69
-
70
- ```html
71
- <table [tanStackTable]="table">
72
- @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
73
- <tr>
74
- @for (header of headerGroup.headers; track header.id) {
75
- <th [tanStackTableHeader]="header">
76
- <ng-container *flexRenderHeader="header; let value">
77
- {{ value }}
78
- </ng-container>
79
- <!-- Any component here can call injectTableHeaderContext() -->
80
- </th>
81
- }
82
- </tr>
83
- } @for (row of table.getRowModel().rows; track row.id) {
84
- <tr>
85
- @for (cell of row.getVisibleCells(); track cell.id) {
86
- <td [tanStackTableCell]="cell">
87
- <ng-container *flexRenderCell="cell; let value">{{ value }}</ng-container>
88
- <app-cell-actions />
89
- <!-- ↑ cell-actions calls injectTableCellContext() — no input drilling -->
90
- </td>
91
- }
92
- </tr>
93
- }
94
- </table>
95
- ```
96
-
97
- ```ts
98
- @Component({
99
- selector: 'app-cell-actions',
100
- template: `<button (click)="cell().row.toggleSelected()">Toggle</button>`,
101
- })
102
- export class CellActionsComponent {
103
- readonly cell = injectTableCellContext()
104
- }
105
- ```
106
-
107
- Each directive uses Angular's `providers` array to register a per-host factory,
108
- so contexts are correctly scoped — multiple `[tanStackTableCell]` directives on
109
- sibling cells provide independent values.
110
-
111
- ---
112
-
113
- ## `*flexRender` directly (custom props)
114
-
115
- Reach for the long form `*flexRender` when:
116
-
117
- - You're rendering something that isn't `columnDef.cell` / `header` / `footer`
118
- (e.g. a registered `headerComponents` member, see `createTableHook`).
119
- - You want to override the props passed to the render function.
120
-
121
- ```html
122
- <ng-container
123
- *flexRender="
124
- cell.column.columnDef.cell;
125
- props: cell.getContext();
126
- let rendered
127
- "
128
- >
129
- {{ rendered }}
130
- </ng-container>
131
- ```
132
-
133
- Custom props (e.g. extending the default context):
134
-
135
- ```html
136
- <ng-container
137
- *flexRender="
138
- cell.column.columnDef.cell;
139
- props: {
140
- ...cell.getContext(),
141
- extra: someValue(),
142
- };
143
- let rendered
144
- "
145
- >
146
- {{ rendered }}
147
- </ng-container>
148
- ```
149
-
150
- Inside the rendered component, `injectFlexRenderContext()` returns this full
151
- props object.
152
-
153
- You can also pass `flexRenderInjector:` to override the injector used for
154
- `createComponent`.
155
-
156
- ---
157
-
158
- ## Render-function injection context
159
-
160
- Because column-def `cell` / `header` / `footer` functions run inside
161
- `runInInjectionContext`, this **works** at the top of those functions:
162
-
163
- ```ts
164
- cell: ({ getValue }) => {
165
- const router = inject(Router) // ✅ legal
166
- return router.url.endsWith('/admin') ? `[admin] ${getValue()}` : getValue()
167
- }
168
- ```
169
-
170
- Be deliberate: this runs every time the cell renders. For per-app values that
171
- don't change, prefer `inject(...)` at the component level and close over.
@@ -1,64 +0,0 @@
1
- # `flexRenderComponent` — Full Options Reference
2
-
3
- When you need custom inputs not derived from the render context, output
4
- callbacks, a custom injector, or Angular v20+ `bindings` / `directives`,
5
- **wrap the component**:
6
-
7
- ```ts
8
- import { flexRenderComponent, type ColumnDef } from '@tanstack/angular-table'
9
- import { EditableCell } from './editable-cell'
10
-
11
- const columns: Array<ColumnDef<typeof features, Person>> = [
12
- {
13
- accessorKey: 'firstName',
14
- cell: ({ getValue, row, column, table }) =>
15
- flexRenderComponent(EditableCell, {
16
- inputs: {
17
- value: getValue(),
18
- },
19
- outputs: {
20
- change: (value) => {
21
- table.options.meta?.updateData(row.index, column.id, value)
22
- },
23
- },
24
- }),
25
- },
26
- ]
27
- ```
28
-
29
- ## How inputs/outputs are wired
30
-
31
- - **`inputs`** → applied via `ComponentRef.setInput(key, value)` (works with
32
- both `input()` signals and legacy `@Input()`). Diffed per change-detection
33
- cycle with `KeyValueDiffers` — unchanged values are _not_ re-set, so object
34
- inputs are reference-checked. Keep input objects referentially stable when
35
- you can.
36
- - **`outputs`** → resolved at component-instance level. The wrapper reads the
37
- property by name, checks it is an `OutputEmitterRef`, and subscribes. The
38
- subscription is cleaned up when the component is destroyed.
39
- - **`injector`** → use when the rendered component needs to inject from a
40
- specific scope (e.g. a feature module / sub-injector).
41
- - **`bindings`** (Angular v20+) → forwarded directly to
42
- `ViewContainerRef.createComponent` at creation time. Use this with
43
- `inputBinding`, `outputBinding`, `twoWayBinding` for native programmatic
44
- rendering semantics.
45
- - **`directives`** (Angular v20+) → host directives forwarded the same way.
46
-
47
- ```ts
48
- import { inputBinding, outputBinding, twoWayBinding, signal } from '@angular/core'
49
-
50
- readonly name = signal('Ada')
51
-
52
- cell: () =>
53
- flexRenderComponent(EditableNameCell, {
54
- bindings: [
55
- inputBinding('value', this.name),
56
- twoWayBinding('value', this.name),
57
- outputBinding('valueChange', (v) => console.log('changed', v)),
58
- ],
59
- })
60
- ```
61
-
62
- > **Do not mix `bindings` with `inputs`/`outputs`** on the same component.
63
- > `bindings` apply at creation time and participate in the initial CD cycle;
64
- > `inputs`/`outputs` apply after. Mixing them risks double-initialization.
@@ -1,468 +0,0 @@
1
- ---
2
- name: angular/client-to-server
3
- description: >
4
- Convert an Angular Table v9 from client-side to server-side processing. Flip
5
- `manualPagination` / `manualSorting` / `manualFiltering` / `manualGrouping` / `manualExpanding`
6
- for the slices the server now owns; drop the corresponding row-model factory slots from the
7
- `features` object for the factories the server replaces; supply `rowCount` (server total) so
8
- pagination computes correctly; hoist `pagination` / `sorting` / `columnFilters` / `globalFilter`
9
- to Angular signals with `state` + `on[State]Change`; fetch via `rxResource` / `httpResource` /
10
- `@tanstack/angular-query`; preserve previous data on refetch with `linkedSignal` (or
11
- `placeholderData: keepPreviousData` for Query); set `getRowId` for stable selection across
12
- refetches.
13
- type: lifecycle
14
- library: tanstack-table
15
- framework: angular
16
- library_version: '9.0.0-alpha.48'
17
- requires:
18
- - angular/table-state
19
- - angular/getting-started
20
- - filtering
21
- - sorting
22
- - pagination
23
- sources:
24
- - TanStack/table:docs/framework/angular/guide/table-state.md
25
- - TanStack/table:docs/framework/angular/guide/migrating.md
26
- - TanStack/table:examples/angular/remote-data/
27
- - TanStack/table:packages/angular-table/src/injectTable.ts
28
- ---
29
-
30
- # Client → Server Conversion (Angular Table v9)
31
-
32
- > Goal: take a working client-side Angular Table v9 and migrate it to server-driven
33
- > processing for one or more of pagination / sorting / filtering / grouping /
34
- > expanding — without rewriting your row markup, columns, or feature surface.
35
- >
36
- > The canonical Angular example is `examples/angular/remote-data/`, using
37
- > `rxResource` + `linkedSignal`. The same pattern composes with
38
- > `@tanstack/angular-query` (see `compose-with-tanstack-query`).
39
-
40
- ---
41
-
42
- ## 1. The 5-step recipe
43
-
44
- For each slice the server now owns:
45
-
46
- 1. **Flip `manualX: true`** in table options. This tells the table "don't
47
- process this on the client — trust the data you receive."
48
- 2. **Drop the matching client-side row-model factory slot** from `features`
49
- (or keep it if you still want the feature's _state_ but no client
50
- recomputation — see §3).
51
- 3. **Hoist the slice to an Angular signal**, control it via `state.x` +
52
- `on[State]Change`. Server requests must depend on the signal.
53
- 4. **Pass `rowCount`** (the server's total) so `getPageCount()`,
54
- `getCanNextPage()`, etc. compute correctly under `manualPagination`.
55
- 5. **Set `getRowId`** so row selection (and refetch identity) survives across
56
- server refetches.
57
-
58
- Plus: keep previous data visible during refetches (avoid a "0-rows flash") with
59
- `linkedSignal`, `placeholderData: keepPreviousData` (Query), or
60
- `previousValue:` (`httpResource`).
61
-
62
- ---
63
-
64
- ## 2. The `manualX` matrix
65
-
66
- | Slice | Option | Client row-model needed? | Notes |
67
- | -------------- | ------------------------ | ------------------------ | -------------------------------------------------------------- |
68
- | Pagination | `manualPagination: true` | drop `paginatedRowModel` | also pass `rowCount: <serverTotal>` |
69
- | Sorting | `manualSorting: true` | drop `sortedRowModel` | feature still controls `sorting` state |
70
- | Column filters | `manualFiltering: true` | drop `filteredRowModel` | also affects global filter when sharing the filtered row model |
71
- | Global filter | `manualFiltering: true` | drop `filteredRowModel` | global filter shares the filtered row model |
72
- | Grouping | `manualGrouping: true` | drop `groupedRowModel` | rare — most servers don't return grouped trees |
73
- | Expanding | `manualExpanding: true` | drop `expandedRowModel` | server returns sub-rows pre-expanded |
74
-
75
- Selection, visibility, ordering, pinning, sizing, resizing, row-pinning are
76
- all UI-only state — they don't have manual modes. They keep working unchanged.
77
-
78
- ---
79
-
80
- ## 3. Canonical example — pagination + sorting + global filter
81
-
82
- The `examples/angular/remote-data/` pattern, condensed:
83
-
84
- ```ts
85
- import {
86
- ChangeDetectionStrategy,
87
- Component,
88
- inject,
89
- linkedSignal,
90
- signal,
91
- } from '@angular/core'
92
- import { rxResource } from '@angular/core/rxjs-interop'
93
- import { HttpClient, HttpParams } from '@angular/common/http'
94
- import { map } from 'rxjs'
95
- import {
96
- FlexRender,
97
- injectTable,
98
- tableFeatures,
99
- rowPaginationFeature,
100
- rowSortingFeature,
101
- columnFilteringFeature,
102
- globalFilteringFeature,
103
- createColumnHelper,
104
- type ColumnDef,
105
- type PaginationState,
106
- type SortingState,
107
- } from '@tanstack/angular-table'
108
-
109
- const features = tableFeatures({
110
- rowPaginationFeature,
111
- rowSortingFeature,
112
- columnFilteringFeature,
113
- globalFilteringFeature,
114
- })
115
-
116
- const columnHelper = createColumnHelper<typeof features, Todo>()
117
-
118
- type TodoResponse = { items: Array<Todo>; totalCount: number }
119
-
120
- @Component({
121
- selector: 'app-root',
122
- imports: [FlexRender],
123
- templateUrl: './app.html',
124
- changeDetection: ChangeDetectionStrategy.OnPush,
125
- })
126
- export class App {
127
- private readonly http = inject(HttpClient)
128
-
129
- // 1. Hoist controlled slices to signals
130
- readonly pagination = signal<PaginationState>({ pageIndex: 0, pageSize: 10 })
131
- readonly sorting = signal<SortingState>([{ id: 'id', desc: false }])
132
- readonly globalFilter = signal<string | null>(null)
133
-
134
- // 2. Fetch — server query depends on those signals
135
- private readonly data = rxResource({
136
- params: () => ({
137
- page: this.pagination(),
138
- sorting: this.sorting(),
139
- globalFilter: this.globalFilter(),
140
- }),
141
- stream: ({ params: { page, sorting, globalFilter } }) => {
142
- let params = new HttpParams({
143
- fromObject: {
144
- _page: page.pageIndex + 1,
145
- _limit: page.pageSize,
146
- },
147
- })
148
- if (globalFilter) params = params.set('title_like', globalFilter)
149
- if (sorting.length) {
150
- params = params
151
- .set('_sort', sorting.map((s) => s.id).join(','))
152
- .set(
153
- '_order',
154
- sorting.map((s) => (s.desc ? 'desc' : 'asc')).join(','),
155
- )
156
- }
157
- return this.http
158
- .get<Array<Todo>>('https://jsonplaceholder.typicode.com/todos', {
159
- params,
160
- observe: 'response',
161
- })
162
- .pipe(
163
- map(
164
- (res) =>
165
- ({
166
- items: res.body ?? [],
167
- totalCount: Number(res.headers.get('X-Total-Count')),
168
- }) satisfies TodoResponse,
169
- ),
170
- )
171
- },
172
- })
173
-
174
- // 3. Keep previous page visible during refetch — no "0 rows" flash
175
- readonly dataWithLatest = linkedSignal<
176
- {
177
- value: TodoResponse | undefined
178
- status: 'idle' | 'loading' | 'resolved' | 'error'
179
- },
180
- TodoResponse
181
- >({
182
- source: () => ({
183
- value: this.data.value(),
184
- status: this.data.status(),
185
- }),
186
- computation: (source, previous) => {
187
- if (previous && source.status === 'loading') return previous.value
188
- return source.value ?? { items: [], totalCount: 0 }
189
- },
190
- })
191
-
192
- readonly columns: Array<ColumnDef<typeof features, Todo>> = [
193
- columnHelper.accessor('id', { header: 'Id', cell: (i) => i.getValue() }),
194
- columnHelper.accessor('title', {
195
- header: 'Title',
196
- cell: (i) => i.getValue(),
197
- }),
198
- columnHelper.accessor('completed', {
199
- header: 'Completed',
200
- cell: (i) => (i.getValue() ? '✅' : '❌'),
201
- }),
202
- ]
203
-
204
- // 4. Wire the table — manualX flags, features without row-model factories, supply rowCount
205
- readonly table = injectTable(() => {
206
- const data = this.dataWithLatest()
207
- return {
208
- features, // ← features has no paginatedRowModel/sortedRowModel/filteredRowModel slots
209
- columns: this.columns,
210
- data: data.items,
211
- getRowId: (row) => String(row.id),
212
-
213
- // Controlled slices
214
- state: {
215
- pagination: this.pagination(),
216
- sorting: this.sorting(),
217
- globalFilter: this.globalFilter(),
218
- },
219
-
220
- // Manual modes
221
- manualPagination: true,
222
- manualSorting: true,
223
- manualFiltering: true,
224
-
225
- // Server's truth about total row count
226
- rowCount: data.totalCount,
227
-
228
- onPaginationChange: (u) =>
229
- typeof u === 'function'
230
- ? this.pagination.update(u)
231
- : this.pagination.set(u),
232
-
233
- onSortingChange: (u) =>
234
- typeof u === 'function' ? this.sorting.update(u) : this.sorting.set(u),
235
-
236
- // When filter changes, also reset page index
237
- onGlobalFilterChange: (u) => {
238
- typeof u === 'function'
239
- ? this.globalFilter.update(u)
240
- : this.globalFilter.set(u)
241
- this.pagination.update((p) => ({ ...p, pageIndex: 0 }))
242
- },
243
- }
244
- })
245
- }
246
- ```
247
-
248
- ### What changed from the client-side version
249
-
250
- - `features` has no `paginatedRowModel`, `sortedRowModel`, or `filteredRowModel`
251
- slots — the server is the source of truth for those.
252
- - `manualPagination` / `manualSorting` / `manualFiltering: true`.
253
- - `rowCount: data.totalCount` — required for correct `getPageCount()` and the
254
- next/prev buttons.
255
- - `state` + per-slice `on[State]Change` for everything the server reads.
256
- - `getRowId` set so row selection survives refetch reorderings.
257
- - `linkedSignal` keeps the previous response visible during loading — without
258
- it, paginating yields a one-frame "no rows" flash because `data.value()` is
259
- `undefined` mid-fetch.
260
- - Resetting `pageIndex` on global-filter change is a UX rule, not framework
261
- behavior — make it explicit.
262
-
263
- ---
264
-
265
- ## 4. Wiring with `@tanstack/angular-query-experimental`
266
-
267
- For Query users, the equivalent of `linkedSignal` is
268
- `placeholderData: keepPreviousData`. See `compose-with-tanstack-query` for the
269
- full pattern. The table-side wiring (manual flags, dropped row models,
270
- controlled signals, `rowCount`, `getRowId`) is identical.
271
-
272
- ---
273
-
274
- ## 5. `rowCount` and friends
275
-
276
- Under `manualPagination: true`, the table no longer knows the total. You must
277
- tell it:
278
-
279
- ```ts
280
- rowCount: data.totalCount // total rows server reports
281
- // pageCount: 42 // can be passed instead, if your API gives pages not rows
282
- ```
283
-
284
- If you omit both, `getPageCount()` returns `-1` and the "next page" button
285
- never disables. If your API reports `pageCount` directly (rare), prefer
286
- `pageCount` — otherwise compute it from `rowCount`.
287
-
288
- ---
289
-
290
- ## 6. Always set `getRowId` when server-driven
291
-
292
- Without `getRowId`, row IDs default to row index. That works on the client
293
- because order is stable per render. On the server, a refetch may return rows in
294
- a different order — `RowSelectionState`, keyed by row ID, then targets the
295
- wrong rows.
296
-
297
- ```ts
298
- getRowId: (row) => row.id
299
- ```
300
-
301
- Required for:
302
-
303
- - `rowSelectionFeature` correctness across refetches
304
- - pinned-row identity
305
- - stable `track row.id` performance in `@for`
306
-
307
- ---
308
-
309
- ## 7. Debouncing rapid input — global filter typing
310
-
311
- Naively, every keystroke triggers a server fetch. Two options:
312
-
313
- - **Manual signal indirection** — keep a `globalFilterInput` signal that the
314
- UI writes to, then update `globalFilter` after a delay via `effect(...) +
315
- setTimeout` or RxJS `debounceTime`.
316
- - **Compose with `@tanstack/angular-pacer`** — see
317
- `compose-with-tanstack-pacer` (not in this batch but on the roadmap).
318
-
319
- Resetting `pageIndex` to 0 when filter or sort changes is a UX standard:
320
-
321
- ```ts
322
- onGlobalFilterChange: (u) => {
323
- typeof u === 'function' ? this.globalFilter.update(u) : this.globalFilter.set(u)
324
- this.pagination.update((p) => ({ ...p, pageIndex: 0 }))
325
- },
326
- ```
327
-
328
- ---
329
-
330
- ## 8. Mixed mode — some slices server, others client
331
-
332
- Common pattern: pagination + sorting on the server, but row selection +
333
- column visibility stay client-only. **Nothing special required** — only the
334
- slices you mark `manualX` are server-driven. Selection / visibility / ordering
335
- work unchanged.
336
-
337
- You can also keep client-side filtering on a column while paginating on the
338
- server, but be wary: if rows are paginated server-side, you only have the
339
- current page to filter against. Usually it's cleaner to flip all data-shape
340
- slices to the server consistently.
341
-
342
- ---
343
-
344
- ## 9. Resetting state on slice changes
345
-
346
- These behaviors are intentional and you'll often want to _override_ them when
347
- server-driven:
348
-
349
- | Default | When server-driven |
350
- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
351
- | `autoResetPageIndex: true` resets `pageIndex` to 0 when data identity changes | OK as-is — every fetch is a new array reference, so the page index resets unless you also pass `autoResetPageIndex: false` |
352
- | Filter change does **not** auto-reset page | UX-standard to reset manually (see §7) |
353
- | Sort change does **not** auto-reset page | Reset manually if your UX expects "new sort → page 1" |
354
-
355
- If your fetch always returns a fresh array, **set `autoResetPageIndex: false`** —
356
- otherwise paginating to page 3 will reset back to page 0 the moment the new
357
- data lands. The remote-data example demonstrates the alternative pattern for
358
- edits (toggle the flag around the update).
359
-
360
- ---
361
-
362
- ## Failure modes
363
-
364
- ### 1. (CRITICAL) Flipping `manualPagination: true` but keeping `paginatedRowModel` on `features`
365
-
366
- The client row-model factory will re-paginate the (already-paginated) data,
367
- chopping the visible rows down to the first `pageSize` of the page slice.
368
- **Remove the `paginatedRowModel` slot from `features`** when going manual — or
369
- accept double-pagination.
370
-
371
- ### 2. (CRITICAL) Forgetting `rowCount` under `manualPagination`
372
-
373
- `getPageCount()` returns `-1`, `getCanNextPage()` is `true` forever,
374
- "page N of -1" appears in the UI. Always pass either `rowCount: serverTotal`
375
- or `pageCount: serverPageCount`.
376
-
377
- ### 3. (CRITICAL) Missing `getRowId` with selection + server refetches
378
-
379
- The row-selection state is keyed by row ID. With index-as-ID, refetches that
380
- return rows in any new order (sort flip, page change) reselect the wrong
381
- rows. Always set `getRowId: row => row.id` (or whatever your primary key is).
382
-
383
- ### 4. (HIGH) "0 rows" flash between pages
384
-
385
- If your fetch resolves to `undefined` during loading, `data.items` becomes `[]`
386
- mid-fetch — the table renders empty for a frame. Use `linkedSignal` (or
387
- `@tanstack/query`'s `placeholderData: keepPreviousData`, or
388
- `httpResource`'s previous-value semantics) to keep the previous page visible.
389
-
390
- ### 5. (HIGH) Forgetting to handle both value AND updater-fn shapes in `on[State]Change`
391
-
392
- ```ts
393
- // ❌ Crashes when the table passes an updater function
394
- onPaginationChange: (value) => this.pagination.set(value)
395
-
396
- // ✅
397
- onPaginationChange: (u) =>
398
- typeof u === 'function' ? this.pagination.update(u) : this.pagination.set(u)
399
- ```
400
-
401
- ### 6. (HIGH) `autoResetPageIndex` resetting your server pagination
402
-
403
- By default, when data identity changes, the table resets to page 0. Under
404
- server-driven pagination, _every_ fetch is a new array, so the table resets
405
- the page index back to 0 every time. Set `autoResetPageIndex: false` and
406
- manage page resets explicitly (e.g. reset on filter/sort change, but not on
407
- the fetch itself).
408
-
409
- ### 7. (HIGH) Filtering on the client when only one page is loaded
410
-
411
- ```ts
412
- manualPagination: true,
413
- // columnFilteringFeature still registered, filteredRowModel slot still on features
414
- ```
415
-
416
- The filtered row model now filters only the _current page_ — useless. If the
417
- server paginates, the server must also filter; flip `manualFiltering: true`
418
- and remove the `filteredRowModel` slot from `features`.
419
-
420
- ### 8. (HIGH) Forgetting to depend on the controlled signals in your fetch
421
-
422
- If your `rxResource` / Query's `queryKey` doesn't read `pagination()`,
423
- `sorting()`, `globalFilter()`, refetches won't happen. Both the table and the
424
- fetcher must observe the same signals.
425
-
426
- ### 9. (MEDIUM) Reimplementing pagination state with raw `pageIndex` /
427
-
428
- `pageSize` signals separate from the table
429
-
430
- ```ts
431
- // ❌ Two sources of truth
432
- readonly pageIndex = signal(0)
433
- readonly pageSize = signal(10)
434
- // table doesn't know about either
435
-
436
- // ✅
437
- readonly pagination = signal<PaginationState>({ pageIndex: 0, pageSize: 10 })
438
- state: { pagination: this.pagination() }
439
- onPaginationChange: ...
440
- ```
441
-
442
- Same lesson: use `setSorting`, not a manual sort signal that the table can't
443
- see.
444
-
445
- ### 10. (MEDIUM) Not resetting `pageIndex` on filter/sort change
446
-
447
- A common bug: user is on page 5, types in the filter, gets "no results" — but
448
- the new filtered result set only has 2 pages. They have to manually click back
449
- to page 1. Always reset `pageIndex` to 0 in `onGlobalFilterChange` /
450
- `onColumnFiltersChange`.
451
-
452
- ### 11. (MEDIUM) Thinking that omitting all row-model slots means "no row models work"
453
-
454
- Core row model is always automatic. `table.getRowModel().rows` returns the
455
- data array as `Row<...>` objects no matter what — a `features` object with no
456
- factory slots just means no client-side processing on top.
457
-
458
- ---
459
-
460
- ## See also
461
-
462
- - `tanstack-table/angular/table-state` — state ownership, `state` vs `atoms`
463
- - `tanstack-table/angular/compose-with-tanstack-query` — server fetch with Query
464
- - `tanstack-table/angular/compose-with-tanstack-store` — external atom ownership
465
- - `tanstack-table/core/filtering` — manualFiltering semantics
466
- - `tanstack-table/core/sorting` — manualSorting semantics
467
- - `tanstack-table/core/pagination` — manualPagination + `rowCount` / `pageCount`
468
- - Example: `examples/angular/remote-data/`