@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,486 @@
1
+ ---
2
+ name: angular/compose-with-tanstack-query
3
+ description: >
4
+ Compose TanStack Table v9 with `@tanstack/angular-query-experimental` for server-side data.
5
+ Key the query on the controlled table state that drives the request (pagination, sorting,
6
+ filters); use `placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages;
7
+ set `manualPagination` / `manualSorting` / `manualFiltering` for the slices the server owns;
8
+ drop the matching client row-model factory slots from `features`; pass `rowCount` from the
9
+ server response; set `getRowId` for stable selection across refetches; hoist controlled slices
10
+ to Angular signals + `state` + `on[State]Change`. Alternative: `rxResource` / `httpResource`
11
+ if you don't want to add the Query dependency (see `client-to-server`).
12
+ type: composition
13
+ library: tanstack-table
14
+ framework: angular
15
+ library_version: '9.0.0-alpha.48'
16
+ requires:
17
+ - angular/table-state
18
+ - angular/client-to-server
19
+ - angular/getting-started
20
+ sources:
21
+ - TanStack/table:docs/framework/angular/guide/table-state.md
22
+ - TanStack/table:examples/angular/remote-data/
23
+ - TanStack/query:packages/angular-query-experimental/src/
24
+ ---
25
+
26
+ # Compose with TanStack Query (Angular)
27
+
28
+ > Goal: server-driven Angular Table v9 with `@tanstack/angular-query-experimental`
29
+ > as the fetch / cache / refetch layer. The pattern is the same as the
30
+ > `examples/angular/remote-data/` example, just with `injectQuery` instead of
31
+ > `rxResource`.
32
+ >
33
+ > The non-Query variant (`rxResource` / `httpResource`) is documented in
34
+ > `tanstack-table/angular/client-to-server`. Both work — Query adds caching,
35
+ > request deduplication, background refetch, and offline coordination.
36
+
37
+ ---
38
+
39
+ ## 1. Install
40
+
41
+ ```bash
42
+ pnpm add @tanstack/angular-query-experimental
43
+ ```
44
+
45
+ Then in `app.config.ts`:
46
+
47
+ ```ts
48
+ import {
49
+ provideTanStackQuery,
50
+ QueryClient,
51
+ } from '@tanstack/angular-query-experimental'
52
+
53
+ export const appConfig: ApplicationConfig = {
54
+ providers: [
55
+ provideTanStackQuery(new QueryClient()),
56
+ // ...
57
+ ],
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 2. The pattern in one snippet
64
+
65
+ ```ts
66
+ import {
67
+ ChangeDetectionStrategy,
68
+ Component,
69
+ computed,
70
+ inject,
71
+ signal,
72
+ } from '@angular/core'
73
+ import { HttpClient, HttpParams } from '@angular/common/http'
74
+ import { lastValueFrom, map } from 'rxjs'
75
+ import {
76
+ injectQuery,
77
+ keepPreviousData,
78
+ } from '@tanstack/angular-query-experimental'
79
+ import {
80
+ FlexRender,
81
+ injectTable,
82
+ tableFeatures,
83
+ rowPaginationFeature,
84
+ rowSortingFeature,
85
+ columnFilteringFeature,
86
+ globalFilteringFeature,
87
+ createColumnHelper,
88
+ type ColumnDef,
89
+ type PaginationState,
90
+ type SortingState,
91
+ } from '@tanstack/angular-table'
92
+
93
+ const features = tableFeatures({
94
+ rowPaginationFeature,
95
+ rowSortingFeature,
96
+ columnFilteringFeature,
97
+ globalFilteringFeature,
98
+ })
99
+
100
+ const columnHelper = createColumnHelper<typeof features, Todo>()
101
+
102
+ type TodoResponse = { items: Array<Todo>; totalCount: number }
103
+
104
+ @Component({
105
+ selector: 'app-root',
106
+ imports: [FlexRender],
107
+ templateUrl: './app.html',
108
+ changeDetection: ChangeDetectionStrategy.OnPush,
109
+ })
110
+ export class App {
111
+ private readonly http = inject(HttpClient)
112
+
113
+ // 1. Hoist controlled slices to Angular signals
114
+ readonly pagination = signal<PaginationState>({ pageIndex: 0, pageSize: 10 })
115
+ readonly sorting = signal<SortingState>([])
116
+ readonly globalFilter = signal<string | null>(null)
117
+
118
+ // 2. Query keyed on those signals — refetches when any of them change
119
+ readonly todosQuery = injectQuery(() => ({
120
+ queryKey: ['todos', this.pagination(), this.sorting(), this.globalFilter()],
121
+ queryFn: () => {
122
+ const p = this.pagination()
123
+ const s = this.sorting()
124
+ const f = this.globalFilter()
125
+
126
+ let params = new HttpParams({
127
+ fromObject: { _page: p.pageIndex + 1, _limit: p.pageSize },
128
+ })
129
+ if (f) params = params.set('title_like', f)
130
+ if (s.length) {
131
+ params = params
132
+ .set('_sort', s.map((x) => x.id).join(','))
133
+ .set('_order', s.map((x) => (x.desc ? 'desc' : 'asc')).join(','))
134
+ }
135
+
136
+ return lastValueFrom(
137
+ this.http
138
+ .get<Array<Todo>>('https://jsonplaceholder.typicode.com/todos', {
139
+ params,
140
+ observe: 'response',
141
+ })
142
+ .pipe(
143
+ map(
144
+ (res) =>
145
+ ({
146
+ items: res.body ?? [],
147
+ totalCount: Number(res.headers.get('X-Total-Count')),
148
+ }) satisfies TodoResponse,
149
+ ),
150
+ ),
151
+ )
152
+ },
153
+ placeholderData: keepPreviousData, // ← prevents "0 rows" flash on refetch
154
+ staleTime: 30_000,
155
+ }))
156
+
157
+ // 3. Stable column defs (module-scope-able)
158
+ readonly columns: Array<ColumnDef<typeof features, Todo>> = [
159
+ columnHelper.accessor('id', { header: 'Id', cell: (i) => i.getValue() }),
160
+ columnHelper.accessor('title', {
161
+ header: 'Title',
162
+ cell: (i) => i.getValue(),
163
+ }),
164
+ columnHelper.accessor('completed', {
165
+ header: 'Completed',
166
+ cell: (i) => (i.getValue() ? '✅' : '❌'),
167
+ }),
168
+ ]
169
+
170
+ // 4. Wire the table — manual flags, features without row-model factories, rowCount, getRowId
171
+ readonly table = injectTable(() => {
172
+ const data = this.todosQuery.data() ?? { items: [], totalCount: 0 }
173
+ return {
174
+ features, // ← features has no paginatedRowModel/sortedRowModel/filteredRowModel slots
175
+ columns: this.columns,
176
+ data: data.items,
177
+ getRowId: (row) => String(row.id),
178
+
179
+ state: {
180
+ pagination: this.pagination(),
181
+ sorting: this.sorting(),
182
+ globalFilter: this.globalFilter(),
183
+ },
184
+
185
+ manualPagination: true,
186
+ manualSorting: true,
187
+ manualFiltering: true,
188
+
189
+ rowCount: data.totalCount, // for getPageCount() under manualPagination
190
+
191
+ autoResetPageIndex: false, // we manage page resets explicitly
192
+
193
+ onPaginationChange: (u) =>
194
+ typeof u === 'function'
195
+ ? this.pagination.update(u)
196
+ : this.pagination.set(u),
197
+ onSortingChange: (u) =>
198
+ typeof u === 'function' ? this.sorting.update(u) : this.sorting.set(u),
199
+ onGlobalFilterChange: (u) => {
200
+ typeof u === 'function'
201
+ ? this.globalFilter.update(u)
202
+ : this.globalFilter.set(u)
203
+ this.pagination.update((p) => ({ ...p, pageIndex: 0 })) // UX reset
204
+ },
205
+ }
206
+ })
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## 3. The four mandatory pieces
213
+
214
+ For server-driven Table + Query to work correctly:
215
+
216
+ 1. **`queryKey` includes every signal the request reads.** If `pagination`
217
+ changes but `queryKey` doesn't include `this.pagination()`, the query
218
+ won't refetch.
219
+ 2. **`placeholderData: keepPreviousData`** keeps the last response visible
220
+ during refetches. Without it, `todosQuery.data()` becomes `undefined`
221
+ mid-fetch, your table shows 0 rows for a frame, the user notices.
222
+ 3. **`manualPagination` / `manualSorting` / `manualFiltering: true`** for
223
+ slices the server owns + **remove the matching factory slots from `features`**
224
+ so the table doesn't re-process the data the server already filtered/sorted/paged.
225
+ 4. **`rowCount: data.totalCount`** (or `pageCount`) so `getPageCount()`
226
+ computes correctly under `manualPagination`.
227
+
228
+ Plus: **`getRowId` for stable identity** across refetches (required for
229
+ correct row selection).
230
+
231
+ ---
232
+
233
+ ## 4. Loading and error UI
234
+
235
+ `injectQuery` returns a signal-rich object. Read the state in templates:
236
+
237
+ ```html
238
+ @if (todosQuery.isPending()) {
239
+ <p>Loading…</p>
240
+ } @else if (todosQuery.isError()) {
241
+ <p>Failed: {{ todosQuery.error()?.message }}</p>
242
+ } @else {
243
+ <table>
244
+ ...
245
+ </table>
246
+ }
247
+ ```
248
+
249
+ With `placeholderData: keepPreviousData`, you'll usually want to show the
250
+ table even while refetching, plus an inline indicator:
251
+
252
+ ```html
253
+ <table>
254
+ <thead>
255
+ ...
256
+ </thead>
257
+ <tbody>
258
+ ...
259
+ </tbody>
260
+ </table>
261
+
262
+ @if (todosQuery.isFetching()) {
263
+ <div class="refreshing-indicator">Refreshing…</div>
264
+ }
265
+ ```
266
+
267
+ `isPending()` is true only for the very first fetch; `isFetching()` is true
268
+ on every background refetch.
269
+
270
+ ---
271
+
272
+ ## 5. Pagination button states
273
+
274
+ Under `manualPagination` + `keepPreviousData`, the next-page button should be
275
+ disabled when there's no more data — but `getCanNextPage()` only knows that
276
+ because you passed `rowCount`. Always pass it:
277
+
278
+ ```html
279
+ <button
280
+ (click)="table.previousPage()"
281
+ [disabled]="!table.getCanPreviousPage() || todosQuery.isFetching()"
282
+ >
283
+
284
+ </button>
285
+ <span
286
+ >Page {{ table.atoms.pagination.get().pageIndex + 1 }} of {{
287
+ table.getPageCount() }}</span
288
+ >
289
+ <button
290
+ (click)="table.nextPage()"
291
+ [disabled]="!table.getCanNextPage() || todosQuery.isFetching()"
292
+ >
293
+
294
+ </button>
295
+ ```
296
+
297
+ Disabling buttons during `isFetching()` prevents double-clicks that fire a
298
+ second refetch.
299
+
300
+ ---
301
+
302
+ ## 6. Row selection across refetches
303
+
304
+ ```ts
305
+ // In the table options
306
+ getRowId: (row) => String(row.id),
307
+ ```
308
+
309
+ Plus register `rowSelectionFeature`. Now `RowSelectionState` is keyed by
310
+ `row.id` (the server primary key) — refetches that change row order don't
311
+ break selection. Without `getRowId`, IDs default to row index and selection
312
+ points at the wrong rows after a sort flip or refetch.
313
+
314
+ ---
315
+
316
+ ## 7. Mutations (cell-level edits)
317
+
318
+ Use Query mutations for cell edits and invalidate the list query on success.
319
+ For inline editing UI, see `compose-with-tanstack-form` (when it ships) — or
320
+ the `examples/angular/editable/` pattern with `flexRenderComponent` and a
321
+ local edit signal.
322
+
323
+ ```ts
324
+ import { inject } from '@angular/core'
325
+ import { injectMutation, QueryClient } from '@tanstack/angular-query-experimental'
326
+ import { lastValueFrom } from 'rxjs'
327
+
328
+ private readonly queryClient = inject(QueryClient)
329
+
330
+ readonly toggleTodoMutation = injectMutation(() => ({
331
+ mutationFn: (id: number) =>
332
+ lastValueFrom(this.http.patch(`/todos/${id}`, { /* … */ })),
333
+ onSuccess: () => {
334
+ this.queryClient.invalidateQueries({ queryKey: ['todos'] })
335
+ },
336
+ }))
337
+
338
+ // In a cell:
339
+ cell: ({ row }) => flexRenderComponent(ToggleButton, {
340
+ inputs: { todo: row.original },
341
+ outputs: { toggle: (id) => this.toggleTodoMutation.mutate(id) },
342
+ })
343
+ ```
344
+
345
+ `invalidateQueries` triggers a background refetch; with `keepPreviousData`,
346
+ the user sees the existing list while the new one loads.
347
+
348
+ ---
349
+
350
+ ## 8. Should I use external `Store` atoms or Angular signals here?
351
+
352
+ For the filter / sort / pagination slices that the Query reads, **either
353
+ works**. The example above uses Angular signals because they read cleanly with
354
+ `@for` and `OnPush`, and Query's `queryKey` polls them on the next CD cycle.
355
+
356
+ Use an external TanStack Store atom (see `compose-with-tanstack-store`) when:
357
+
358
+ - The same slice must drive multiple tables.
359
+ - You're syncing the slice to the URL or `localStorage`.
360
+ - Other non-table consumers in the app already use the atom.
361
+
362
+ In those cases:
363
+
364
+ ```ts
365
+ import { paginationStore } from './stores'
366
+ import { injectSelector } from '@tanstack/angular-store'
367
+
368
+ readonly paginationSig = injectSelector(paginationStore, (s) => s)
369
+
370
+ readonly todosQuery = injectQuery(() => ({
371
+ queryKey: ['todos', this.paginationSig() /*, sort, filter */],
372
+ // ...
373
+ }))
374
+
375
+ readonly table = injectTable(() => ({
376
+ // ...
377
+ atoms: { pagination: paginationStore },
378
+ // no state.pagination needed — the atom owns it
379
+ }))
380
+ ```
381
+
382
+ ---
383
+
384
+ ## Failure modes
385
+
386
+ ### 1. (CRITICAL) `queryKey` missing the controlled signals
387
+
388
+ ```ts
389
+ // ❌ Query won't refetch when pagination changes
390
+ queryKey: ['todos'],
391
+
392
+ // ✅
393
+ queryKey: ['todos', this.pagination(), this.sorting(), this.globalFilter()],
394
+ ```
395
+
396
+ Symptom: paginating "doesn't load the next page." Always include every signal
397
+ the request reads.
398
+
399
+ ### 2. (CRITICAL) No `placeholderData: keepPreviousData` → "0 rows flash"
400
+
401
+ Without it, `data()` is `undefined` mid-refetch, the table renders 0 rows
402
+ for a frame. `keepPreviousData` (imported from
403
+ `@tanstack/angular-query-experimental`) keeps the last successful payload
404
+ visible until the new one resolves.
405
+
406
+ ### 3. (CRITICAL) Forgetting `rowCount` under `manualPagination`
407
+
408
+ `getPageCount()` returns `-1`, "next" never disables. The server tells you
409
+ how many rows exist — pass it.
410
+
411
+ ### 4. (CRITICAL) Keeping client row-model factory slots for slices the server owns
412
+
413
+ ```ts
414
+ // ❌ Double-processes the data
415
+ manualPagination: true,
416
+ // features still has paginatedRowModel slot → re-paginates server page
417
+
418
+ // ✅ remove the factory slot from features for server-owned slices
419
+ const features = tableFeatures({
420
+ rowPaginationFeature,
421
+ // paginatedRowModel intentionally omitted — server handles it
422
+ })
423
+ ```
424
+
425
+ Same applies to `sortedRowModel` under `manualSorting` and `filteredRowModel`
426
+ under `manualFiltering`.
427
+
428
+ ### 5. (CRITICAL) Missing `getRowId` with row selection
429
+
430
+ Selection is keyed by row ID. Index-as-ID breaks when the server returns rows
431
+ in a new order. `getRowId: (row) => row.id`.
432
+
433
+ ### 6. (HIGH) `autoResetPageIndex` bouncing the user to page 0 on every refetch
434
+
435
+ Every Query response is a new array reference → table sees "new data" → resets
436
+ `pageIndex`. Set `autoResetPageIndex: false` and reset explicitly when you
437
+ want to (e.g. in `onGlobalFilterChange`).
438
+
439
+ ### 7. (HIGH) Refetching on every keystroke
440
+
441
+ Typing into the global filter fires a fetch per character. Debounce: either
442
+ keep a separate `globalFilterInput` signal and propagate to `globalFilter` on
443
+ a delay, or compose with `@tanstack/angular-pacer` (when its skill ships).
444
+ Also reset `pageIndex: 0` on filter change.
445
+
446
+ ### 8. (HIGH) Forgetting the updater-fn branch in `on[State]Change`
447
+
448
+ ```ts
449
+ // ❌ Crashes when TanStack Table passes a function (e.g. table.setPageIndex((p) => p + 1))
450
+ onPaginationChange: (value) => this.pagination.set(value)
451
+
452
+ // ✅
453
+ onPaginationChange: (u) =>
454
+ typeof u === 'function' ? this.pagination.update(u) : this.pagination.set(u)
455
+ ```
456
+
457
+ ### 9. (MEDIUM) Reaching for `effect(...)` to call `query.refetch()` on signal changes
458
+
459
+ Don't. The whole point of `queryKey` is that Query refetches when the key
460
+ changes. Adding an `effect` that calls `refetch()` produces double fetches and
461
+ race conditions. Trust the key.
462
+
463
+ ### 10. (MEDIUM) Two sources of truth for filter state
464
+
465
+ Common bug: keep a `signal('')` for the input AND a `state.globalFilter`
466
+ controlled value, and try to sync them via `effect`. Pick one: the table's
467
+ `globalFilter` is fine for both UI and server query. If you need debouncing,
468
+ use an _additional_ `globalFilterInput` signal for the raw input and update
469
+ the table-controlled signal on a delay.
470
+
471
+ ---
472
+
473
+ ## See also
474
+
475
+ - `tanstack-table/angular/client-to-server` — the no-Query baseline using
476
+ `rxResource` / `httpResource`
477
+ - `tanstack-table/angular/table-state` — Angular signal + `state` + `on*Change`
478
+ - `tanstack-table/angular/compose-with-tanstack-store` — when to use shared
479
+ atoms instead of signals
480
+ - `tanstack-table/core/filtering` — manualFiltering semantics
481
+ - `tanstack-table/core/sorting` — manualSorting semantics
482
+ - `tanstack-table/core/pagination` — manualPagination + `rowCount`
483
+ - Example: `examples/angular/remote-data/` — analogous pattern with
484
+ `rxResource` instead of `injectQuery`
485
+ - `@tanstack/angular-query-experimental` docs for `injectQuery`,
486
+ `injectMutation`, `injectInfiniteQuery`, `QueryClient`