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

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