@tanstack/angular-table 9.0.0-alpha.9 → 9.0.0-beta.10

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 (54) hide show
  1. package/README.md +127 -0
  2. package/dist/README.md +127 -0
  3. package/dist/fesm2022/tanstack-angular-table-static-functions.mjs +6 -0
  4. package/dist/fesm2022/tanstack-angular-table-static-functions.mjs.map +1 -0
  5. package/dist/fesm2022/tanstack-angular-table.mjs +1299 -248
  6. package/dist/fesm2022/tanstack-angular-table.mjs.map +1 -1
  7. package/dist/types/tanstack-angular-table-static-functions.d.ts +1 -0
  8. package/dist/types/tanstack-angular-table.d.ts +787 -0
  9. package/package.json +39 -19
  10. package/skills/angular/angular-rendering-directives/SKILL.md +415 -0
  11. package/skills/angular/angular-rendering-directives/references/content-shapes.md +142 -0
  12. package/skills/angular/angular-rendering-directives/references/create-table-hook-registries.md +89 -0
  13. package/skills/angular/angular-rendering-directives/references/di-tokens.md +171 -0
  14. package/skills/angular/angular-rendering-directives/references/flex-render-component-options.md +64 -0
  15. package/skills/angular/client-to-server/SKILL.md +468 -0
  16. package/skills/angular/compose-with-tanstack-query/SKILL.md +486 -0
  17. package/skills/angular/compose-with-tanstack-store/SKILL.md +405 -0
  18. package/skills/angular/compose-with-tanstack-virtual/SKILL.md +399 -0
  19. package/skills/angular/getting-started/SKILL.md +487 -0
  20. package/skills/angular/getting-started/references/feature-row-model-mapping.md +51 -0
  21. package/skills/angular/migrate-v8-to-v9/SKILL.md +422 -0
  22. package/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md +268 -0
  23. package/skills/angular/production-readiness/SKILL.md +472 -0
  24. package/skills/angular/table-state/SKILL.md +422 -0
  25. package/skills/angular/table-state/references/external-atoms-and-app-hook.md +153 -0
  26. package/src/flex-render/context.ts +14 -0
  27. package/src/flex-render/flags.ts +34 -0
  28. package/src/flex-render/flexRenderComponent.ts +288 -0
  29. package/src/flex-render/flexRenderComponentFactory.ts +251 -0
  30. package/src/flex-render/renderer.ts +393 -0
  31. package/src/flex-render/view.ts +226 -0
  32. package/src/flexRender.ts +124 -0
  33. package/src/helpers/cell.ts +108 -0
  34. package/src/helpers/createTableHook.ts +498 -0
  35. package/src/helpers/flexRenderCell.ts +136 -0
  36. package/src/helpers/header.ts +101 -0
  37. package/src/helpers/table.ts +87 -0
  38. package/src/index.ts +23 -70
  39. package/src/injectTable.ts +146 -0
  40. package/src/{lazy-signal-initializer.ts → lazySignalInitializer.ts} +2 -2
  41. package/src/reactivity.ts +105 -0
  42. package/static-functions/index.ts +1 -0
  43. package/static-functions/ng-package.json +6 -0
  44. package/dist/esm2022/flex-render.mjs +0 -148
  45. package/dist/esm2022/index.mjs +0 -48
  46. package/dist/esm2022/lazy-signal-initializer.mjs +0 -43
  47. package/dist/esm2022/proxy.mjs +0 -83
  48. package/dist/esm2022/tanstack-angular-table.mjs +0 -5
  49. package/dist/flex-render.d.ts +0 -30
  50. package/dist/index.d.ts +0 -5
  51. package/dist/lazy-signal-initializer.d.ts +0 -5
  52. package/dist/proxy.d.ts +0 -3
  53. package/src/flex-render.ts +0 -184
  54. package/src/proxy.ts +0 -97
@@ -0,0 +1,468 @@
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/`