@tanstack/angular-table 9.0.0-beta.4 → 9.0.0-beta.43

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