@tanstack/angular-table 9.0.0-alpha.47 → 9.0.0-alpha.48

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.
@@ -0,0 +1,400 @@
1
+ ---
2
+ name: angular/compose-with-tanstack-virtual
3
+ description: >
4
+ Compose TanStack Table v9 with `@tanstack/angular-virtual` for virtualized rendering of large
5
+ row sets. TanStack Table does NOT virtualize on its own. Pattern: get `rows = table.getRowModel().rows`,
6
+ feed `rows.length` to `injectVirtualizer({ count, estimateSize, getScrollElement, overscan })`,
7
+ iterate `virtualizer.getVirtualItems()` in the template, position each row with
8
+ `transform: translateY(item.start)` inside a tall sentinel, set
9
+ `[style.height.px]="virtualizer.getTotalSize()"` to make the scrollbar correct. Handle the
10
+ table-feature interactions: row-expanding (variable subRow heights → measure with
11
+ `measureElement`), column sizing/pinning (column virtualization is separate),
12
+ row-selection (selection state survives virtualization because it's keyed by row ID).
13
+ type: composition
14
+ library: tanstack-table
15
+ framework: angular
16
+ library_version: '9.0.0-alpha.47'
17
+ requires:
18
+ - angular/table-state
19
+ - angular/getting-started
20
+ - angular/angular-rendering-directives
21
+ sources:
22
+ - TanStack/table:docs/framework/angular/angular-table.md
23
+ - TanStack/virtual:packages/angular-virtual/src/
24
+ - TanStack/table:examples/angular/basic-inject-table/
25
+ ---
26
+
27
+ # Compose with TanStack Virtual (Angular)
28
+
29
+ > TanStack Table is headless — it computes which rows / cells exist, but does
30
+ > not decide which ones to render to the DOM. For tables larger than a few
31
+ > hundred visible rows, pair with [`@tanstack/angular-virtual`](https://tanstack.com/virtual)
32
+ > so only the rows in the viewport (+ overscan) actually mount.
33
+ >
34
+ > Required reading: `tanstack-table/angular/getting-started` and
35
+ > `tanstack-table/angular/table-state`.
36
+
37
+ ---
38
+
39
+ ## 1. Install
40
+
41
+ ```bash
42
+ pnpm add @tanstack/angular-virtual
43
+ ```
44
+
45
+ Requires the same Angular version as `@tanstack/angular-table`.
46
+
47
+ ---
48
+
49
+ ## 2. The integration in one shape
50
+
51
+ ```ts
52
+ import {
53
+ AfterViewInit,
54
+ ChangeDetectionStrategy,
55
+ Component,
56
+ computed,
57
+ signal,
58
+ viewChild,
59
+ ElementRef,
60
+ } from '@angular/core'
61
+ import {
62
+ FlexRender,
63
+ injectTable,
64
+ tableFeatures,
65
+ type ColumnDef,
66
+ } from '@tanstack/angular-table'
67
+ import { injectVirtualizer } from '@tanstack/angular-virtual'
68
+
69
+ const _features = tableFeatures({})
70
+
71
+ @Component({
72
+ selector: 'app-virtual-table',
73
+ imports: [FlexRender],
74
+ templateUrl: './virtual-table.html',
75
+ styleUrl: './virtual-table.css',
76
+ changeDetection: ChangeDetectionStrategy.OnPush,
77
+ })
78
+ export class VirtualTable {
79
+ readonly data = signal<Array<Person>>(makeData(50_000))
80
+ readonly scrollContainer =
81
+ viewChild.required<ElementRef<HTMLDivElement>>('scroll')
82
+
83
+ readonly table = injectTable(() => ({
84
+ _features,
85
+ _rowModels: {},
86
+ columns,
87
+ data: this.data(),
88
+ getRowId: (row) => row.id,
89
+ }))
90
+
91
+ // Stable reference to the rows array for the virtualizer
92
+ readonly rows = computed(() => this.table.getRowModel().rows)
93
+
94
+ readonly rowVirtualizer = injectVirtualizer(() => ({
95
+ count: this.rows().length,
96
+ getScrollElement: () => this.scrollContainer().nativeElement,
97
+ estimateSize: () => 36, // fixed-height rows
98
+ overscan: 10,
99
+ }))
100
+ }
101
+ ```
102
+
103
+ ```html
104
+ <!-- virtual-table.html -->
105
+ <div #scroll class="scroll-container" style="height: 600px; overflow: auto">
106
+ <table style="display: grid">
107
+ <thead style="display: grid; position: sticky; top: 0; z-index: 1">
108
+ @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
109
+ <tr style="display: flex; width: 100%">
110
+ @for (header of headerGroup.headers; track header.id) {
111
+ <th style="display: flex">
112
+ <ng-container *flexRenderHeader="header; let value"
113
+ >{{ value }}</ng-container
114
+ >
115
+ </th>
116
+ }
117
+ </tr>
118
+ }
119
+ </thead>
120
+
121
+ <tbody
122
+ style="display: grid; position: relative"
123
+ [style.height.px]="rowVirtualizer.getTotalSize()"
124
+ >
125
+ @for (virtualRow of rowVirtualizer.getVirtualItems(); track
126
+ virtualRow.key) { @let row = rows()[virtualRow.index];
127
+ <tr
128
+ [attr.data-index]="virtualRow.index"
129
+ style="display: flex; position: absolute; width: 100%"
130
+ [style.transform]="'translateY(' + virtualRow.start + 'px)'"
131
+ >
132
+ @for (cell of row.getVisibleCells(); track cell.id) {
133
+ <td style="display: flex">
134
+ <ng-container *flexRenderCell="cell; let value"
135
+ >{{ value }}</ng-container
136
+ >
137
+ </td>
138
+ }
139
+ </tr>
140
+ }
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+ ```
145
+
146
+ ### What's doing what
147
+
148
+ - **Table** produces `rows` (`table.getRowModel().rows`). Length, identity,
149
+ order are decided by registered features (sort, filter, pagination,
150
+ grouping).
151
+ - **Virtualizer** turns `rows.length` into the subset of "virtual items"
152
+ currently visible (+ `overscan`). It tracks scroll on `getScrollElement()`
153
+ and emits `getVirtualItems()` keyed by `virtualRow.key` (the row index by
154
+ default).
155
+ - **Template** renders only the virtual items, positions each with
156
+ `translateY(virtualRow.start)` inside a sentinel of total height
157
+ `getTotalSize()`. The scrollbar reflects the full row count, but only the
158
+ visible window has DOM nodes.
159
+
160
+ ---
161
+
162
+ ## 3. Mandatory layout details
163
+
164
+ This integration touches CSS in a few non-obvious places. None are optional:
165
+
166
+ - **Scroll container has a fixed height** (`height: 600px` / `100vh` /
167
+ whatever) **and `overflow: auto`**. The virtualizer needs both to compute
168
+ visible range.
169
+ - **Use `display: grid` on `<table>`, `display: flex` on `<thead>` /
170
+ `<tbody>` / `<tr>`**, or use `<div>` markup. Native `<table>` layout
171
+ defeats positioning rows absolutely. The virtual example above uses CSS
172
+ grid to keep semantic table markup while letting the rows position freely.
173
+ - **Container `<tbody>` is `position: relative` with explicit
174
+ `height = virtualizer.getTotalSize()`.** Without that height, the scrollbar
175
+ doesn't reflect the full data.
176
+ - **Rows are `position: absolute; top: 0; left: 0; width: 100%` with
177
+ `transform: translateY(virtualRow.start)`.**
178
+ - **Sticky header**: `position: sticky; top: 0; z-index: 1` on the `<thead>`
179
+ / its `<tr>` — the scroll container provides the scrolling.
180
+
181
+ ---
182
+
183
+ ## 4. Variable row heights — measure dynamically
184
+
185
+ When rows can be different heights (expanded subRows, dynamic cell content),
186
+ pass `measureElement` and a sensible `estimateSize`:
187
+
188
+ ```ts
189
+ readonly rowVirtualizer = injectVirtualizer(() => ({
190
+ count: this.rows().length,
191
+ getScrollElement: () => this.scrollContainer().nativeElement,
192
+ estimateSize: () => 36,
193
+ overscan: 10,
194
+ measureElement: (element) => element?.getBoundingClientRect().height ?? 36,
195
+ }))
196
+ ```
197
+
198
+ In the template, bind the element so the virtualizer can measure it:
199
+
200
+ ```html
201
+ <tr
202
+ #rowEl
203
+ [attr.data-index]="virtualRow.index"
204
+ [virtualizerMeasureElement]="rowVirtualizer"
205
+ ...
206
+ ></tr>
207
+ ```
208
+
209
+ (See `@tanstack/angular-virtual` docs for the exact directive name and API;
210
+ the principle is: every mounted row reports its real size, the virtualizer
211
+ caches that, scrollbar adjusts.)
212
+
213
+ ---
214
+
215
+ ## 5. Row expanding — `rowExpandingFeature`
216
+
217
+ Combine with `rowExpandingFeature` for "click to expand details":
218
+
219
+ - Register `rowExpandingFeature` in `_features` and
220
+ `expandedRowModel: createExpandedRowModel()` in `_rowModels`.
221
+ - Use `table.getExpandedRowModel().rows` (or `getRowModel().rows`, which
222
+ already includes expansion under `paginateExpandedRows: true` semantics —
223
+ see `tanstack-table/core/row-expanding`).
224
+ - **Always use `measureElement`** because expansion changes row heights.
225
+ - The virtualizer keys items by index; expanded subRows shift later rows
226
+ down — that's correct and expected.
227
+
228
+ ---
229
+
230
+ ## 6. Row selection works transparently
231
+
232
+ Row selection is keyed by row ID (`getRowId`), not by DOM presence. A row can
233
+ be selected while off-screen; scrolling it into view shows the right checkbox
234
+ state. **Always set `getRowId`** — critical for both selection and
235
+ virtualizer key stability.
236
+
237
+ ---
238
+
239
+ ## 7. Column virtualization (horizontal)
240
+
241
+ For very wide tables (50+ columns), virtualize columns too — a second
242
+ `injectVirtualizer` over `table.getVisibleLeafColumns().length`. The pattern
243
+ mirrors row virtualization but on the X axis. Combine with
244
+ `columnPinningFeature` so pinned columns escape the virtualizer (always
245
+ rendered, sticky).
246
+
247
+ That's a meaningfully bigger lift — most tables don't need it. Reach for it
248
+ only when you've profiled and column count is the bottleneck.
249
+
250
+ ---
251
+
252
+ ## 8. Interaction with pagination
253
+
254
+ **If you paginate, you usually don't virtualize.** Pagination already caps
255
+ the rendered row count to `pageSize`. Adding virtualization on top is
256
+ typically wasted effort — you've already solved the rendering bottleneck.
257
+
258
+ The exceptions:
259
+
260
+ - Pages can hold thousands of rows (rare).
261
+ - Pagination is "load more" / infinite scroll style — then virtualize the
262
+ accumulated rows.
263
+
264
+ ---
265
+
266
+ ## 9. Interaction with sticky / pinned rows
267
+
268
+ `rowPinningFeature` + virtualization is fiddly. Pinned rows live at the
269
+ top/bottom of the table; they should render _outside_ the virtualizer's
270
+ absolute positioning. Render them in dedicated `<thead>` /
271
+ top/bottom-of-`<tbody>` sections, and call `table.getCenterRows()` (the
272
+ non-pinned rows) to feed the virtualizer. See
273
+ `tanstack-table/core/row-pinning` for the API surface.
274
+
275
+ ---
276
+
277
+ ## 10. SSR / first-paint
278
+
279
+ On the server / first hydration, the scroll container's height is unknown;
280
+ the virtualizer can render zero rows. Two mitigations:
281
+
282
+ - Render a small initial chunk server-side (without the virtualizer) and let
283
+ Angular hydrate into the virtualized version client-side.
284
+ - Provide an explicit `initialRect: { width, height }` to the virtualizer
285
+ options for SSR.
286
+
287
+ ---
288
+
289
+ ## Failure modes
290
+
291
+ ### 1. (CRITICAL) Trying to use TanStack Table's own virtualization
292
+
293
+ There is none. TanStack Table doesn't ship a virtualizer. If an agent
294
+ suggests `getVirtualizedRows()` or `enableVirtualization: true` on the table —
295
+ those don't exist. Use `@tanstack/angular-virtual`.
296
+
297
+ ### 2. (CRITICAL) Missing height on the scroll container
298
+
299
+ ```html
300
+ <!-- ❌ no height → virtualizer reports 0 visible items → nothing renders -->
301
+ <div #scroll style="overflow: auto">
302
+ <!-- ✅ -->
303
+ <div #scroll style="height: 600px; overflow: auto"></div>
304
+ </div>
305
+ ```
306
+
307
+ The virtualizer measures the _scroll element_'s viewport. Without an explicit
308
+ or computed height, the viewport is 0 and nothing renders.
309
+
310
+ ### 3. (CRITICAL) Missing `getTotalSize()` height on the row container
311
+
312
+ ```html
313
+ <!-- ❌ scrollbar reflects only the rendered rows, not the full dataset -->
314
+ <tbody>
315
+ <!-- ✅ -->
316
+ </tbody>
317
+
318
+ <tbody [style.height.px]="rowVirtualizer.getTotalSize()"></tbody>
319
+ ```
320
+
321
+ Without this, you can scroll to the bottom of the _visible_ rows but can
322
+ never reach row 1000. The scrollbar lies.
323
+
324
+ ### 4. (CRITICAL) Forgetting `transform: translateY(...)` per row
325
+
326
+ Absolutely-positioned rows without `transform` stack at `top: 0` — every row
327
+ renders on top of every other.
328
+
329
+ ### 5. (CRITICAL) Using native `<table>` layout with absolute-positioned rows
330
+
331
+ Native `<table>` layout overrides positioning on `<tr>` / `<td>`. Either:
332
+
333
+ - Use `display: grid` on `<table>` and `display: flex` on `<tr>` / `<td>`
334
+ (see §3), preserving semantic markup, OR
335
+ - Use `<div>` markup throughout.
336
+
337
+ ### 6. (HIGH) Variable row heights without `measureElement`
338
+
339
+ Default `estimateSize` is a constant. Different real heights → wrong
340
+ positions → rows visually overlap or leave gaps. Pass `measureElement` and a
341
+ way for each mounted row to report its real size.
342
+
343
+ ### 7. (HIGH) Pagination + virtualization both enabled
344
+
345
+ Pagination already caps row count. Adding virtualization on top doubles the
346
+ indirection for no win. Pick one.
347
+
348
+ ### 8. (HIGH) Reimplementing virtualization with `IntersectionObserver`
349
+
350
+ Saw an agent build a homegrown "render rows when visible" with
351
+ `IntersectionObserver`? That's hundreds of lines of broken virtualization.
352
+ Use the library.
353
+
354
+ ### 9. (HIGH) Wrong `track` in the virtualized `@for`
355
+
356
+ ```html
357
+ <!-- ❌ tracking by row.id confuses Angular because positions shift -->
358
+ @for (virtualRow of rowVirtualizer.getVirtualItems(); track row.id)
359
+
360
+ <!-- ✅ -->
361
+ @for (virtualRow of rowVirtualizer.getVirtualItems(); track virtualRow.key)
362
+ ```
363
+
364
+ Track by the virtual item's stable key (or index). The row is _inside_ the
365
+ virtual item — Angular uses the outer track for DOM reuse.
366
+
367
+ ### 10. (MEDIUM) `injectVirtualizer` outside an injection context
368
+
369
+ Like `injectTable`, `injectVirtualizer` calls `assertInInjectionContext()`.
370
+ Place it on a class field, in a constructor, or inside `runInInjectionContext`.
371
+
372
+ ### 11. (MEDIUM) Recreating `count` / `estimateSize` on every signal change without
373
+
374
+ stable callbacks
375
+
376
+ Move `estimateSize`, `measureElement`, `getScrollElement` to stable
377
+ references (class arrow methods or module-scope functions) where possible.
378
+ Otherwise the virtualizer re-initializes its internal state on every change.
379
+
380
+ ### 12. (MEDIUM) Missing `getRowId` — selection breaks across re-sorts in a
381
+
382
+ virtualized table
383
+
384
+ This isn't virtualization-specific, but it's especially visible here because
385
+ virtualization renders a window of rows; refreshing that window via scroll
386
+ makes mismatched checkbox state obvious. `getRowId: (row) => row.id` is
387
+ mandatory.
388
+
389
+ ---
390
+
391
+ ## See also
392
+
393
+ - `tanstack-table/angular/getting-started` — baseline table that this skill
394
+ layers virtualization on top of
395
+ - `tanstack-table/angular/production-readiness` — when to reach for
396
+ virtualization vs server-side pagination
397
+ - `tanstack-table/core/row-expanding` — variable subRow heights + virtual
398
+ - `tanstack-table/core/column-layout` — pinning interaction
399
+ - `@tanstack/angular-virtual` docs — `injectVirtualizer`, options reference,
400
+ variable-height patterns