Skip to content

Commit e46e600

Browse files
serpentbladeclaude
andcommitted
docs(53-05): retire virtualization deferral, publish tested-to ceiling (100,000 rows)
- Comparison: move row virtualization OUT of 'What Rozie defers' into a wins bullet + feature-matrix row; publish falsifiable 'tested to 100,000 rows' ceiling (== VR fixture count, D-13/A4) - Comparison: log remaining deferrals (horizontal/column virtualization + dynamic auto-measure beyond measureElement); bump prop count 15→18 - Demo: add a 50,000-row windowing live demo (virtual + maxHeight) + showcase prose - Usage: regenerated with the third virtualization example set; docs build green (gen + vitepress + check-anchors, 162 anchors / 0 broken) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wvxhsB5hcDKD8UadLLSyw
1 parent 140f217 commit e46e600

3 files changed

Lines changed: 218 additions & 2 deletions

File tree

docs/components/data-table-comparison.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Cell legend: **✅** = documented out-of-the-box · **❌** = not supported / no
4040
| Row selection |||||||`selectionMode` |
4141
| Column visibility / resize / reorder / pin ||||| ✅ (AG) || ✅ four slices |
4242
| Sticky header ||| ⚠️ ||||`stickyHeader` |
43+
| Row virtualization (windowing) | ✅ (TanStack Virtual) | ✅ (TanStack Virtual) | ✅ (TanStack Virtual) | ✅ (TanStack Virtual) | ✅ (AG) ||`virtual` (tested to 100,000 rows) |
4344
| APG grid keyboard navigation (`role="grid"`) || ⚠️ (PrimeVue) ||| ✅ (AG) ||`interactionMode="grid"` |
4445
| Declarative `<Column>` surface | ⚠️ defs array | ✅ (PrimeVue) | ⚠️ | ⚠️ | ⚠️ ||`<Column>` + `:columns` |
4546
| Custom cell / header rendering ||||||| ✅ parent `#cell` / `#colHeader`, `columnId`-dispatched (React/Solid render-prop, Lit property) |
@@ -58,15 +59,16 @@ Cell legend: **✅** = documented out-of-the-box · **❌** = not supported / no
5859
- **A declarative `<Column>` API** (the PrimeVue-shaped surface) *and* a `:columns` config-array escape hatch, resolved by an id-keyed last-write-wins union — plus custom cell/header rendering via a single parent `#cell` / `#colHeader` scoped slot dispatched by `columnId` (a render-prop on React/Solid and a property on Lit, the one documented divergence).
5960
- **Zero-config styling that re-skins to any design system.** Every rendered value is a `--rozie-data-table-*` CSS custom property with a built-in fallback, plus ready-made token bridges for shadcn/ui, Material 3, and Bootstrap 5 — no required CSS import.
6061
- **Opt-in WAI-ARIA grid mode, identical on all six targets.** Set `interactionMode="grid"` for the full [APG grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/)`role="grid"`, a roving single tab-stop, and 2-D arrow-key cell navigation that survives a re-sort / filter / page change / column hide-reorder-pin (the active cell is tracked as a `{ rowIndex, colIndex }` pair over the visible model, never a stored DOM node). It is drivable and observable via the `focusCell` / `getActiveCell` / `clearActiveCell` handle verbs and the `activecell-change` event, and the behavioral contract is locked by a cross-framework VR matrix. The default `interactionMode="table"` stays a plain accessible table, byte-for-byte unchanged.
62+
- **Opt-in vertical row windowing, on all six targets.** Set `virtual` (with an optional `maxHeight` / `--rozie-data-table-max-height` and `estimateRowHeight`) to render only the visible slice of a large dataset inside a bounded scroll container — windowing the full filtered + sorted (pre-pagination) model, with `aria-rowcount` / `aria-rowindex` mapping the full model, variable-height rows measured (no cumulative drift), and sticky-header + pinned-column geometry preserved. Built on the framework-agnostic `@tanstack/virtual-core` wired by hand — **no per-framework virtual adapter** — and **tested to 100,000 rows** on all six targets by a DOM/behavioral VR matrix (`tests/visual-regression/specs/data-table-virtual.spec.ts`). The default `virtual="false"` is byte-identical to a non-virtual table.
6163

6264
## What Rozie defers {#what-rozie-defers}
6365

6466
This page concedes where the incumbents are genuinely ahead — that's what keeps the comparison credible, and it doubles as Rozie's own roadmap.
6567

6668
- **AG Grid's enterprise depth.** [AG Grid](https://www.ag-grid.com/) ships row grouping, tree data, pivoting, range selection, master/detail, integrated charting, and a deep server-side row model. `@rozie-ui/data-table` covers the common surface (sort / filter / paginate / select / column management) and a `manual` server-side hook, not the enterprise feature set.
67-
- **Row/cell virtualization.** Large datasets render every row today — there is no built-in windowing. Virtualization is a planned phase (the published support boundary is a row-count ceiling until it lands); for now, paginate or pre-window large data yourself.
69+
- **Horizontal (column) virtualization + dynamic auto-measure.** Vertical **row** windowing ships and is GA on all six targets — set `virtual` and large datasets render only the visible slice (**tested to 100,000 rows**). What remains deferred is the orthogonal pieces: **horizontal/column virtualization** (a very wide column set still renders every column), and **dynamic auto-measurement beyond `measureElement`** — variable row heights are measured via `measureElement`, but there is no content-driven auto-sizing pass past it. Use `estimateRowHeight` to seed the row estimate for now.
6870
- **TanStack's expansion / grouping / faceting helpers.** The shared `table-core` exposes more row-model features (expanding, grouping, faceted filters) than `@rozie-ui/data-table` surfaces in its current prop set. They are reachable via the imperative handle / future additive props.
69-
- **`@rozie-ui/data-table` is `0.1.0`.** The surface (15 props / 9 two-way slices / 9 events / 15-verb handle / declarative `<Column>` + `columnId`-dispatched cell/header slots / 2 selection slots) is stable and gate-verified across all six targets, but it is younger and less battle-tested than the established libraries.
71+
- **`@rozie-ui/data-table` is `0.1.0`.** The surface (18 props / 9 two-way slices / 9 events / 15-verb handle / declarative `<Column>` + `columnId`-dispatched cell/header slots / 2 selection slots) is stable and gate-verified across all six targets, but it is younger and less battle-tested than the established libraries.
7072

7173
## Try it
7274

docs/components/data-table-demo.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ const rowSelection = ref({});
2424
const pagination = ref({ pageIndex: 0, pageSize: 5 });
2525

2626
const tbl = ref();
27+
28+
// A large dataset for the row-windowing demo — 50,000 synthetic rows. With
29+
// `virtual`, only the visible slice renders inside the bounded scroll container.
30+
const BIG_ROWS = Array.from({ length: 50_000 }, (_, i) => ({
31+
id: i + 1,
32+
name: `Row ${i + 1}`,
33+
email: `user${i + 1}@example.com`,
34+
status: i % 2 ? 'active' : 'away',
35+
}));
2736
</script>
2837

2938
# DataTable — live demo
@@ -73,6 +82,24 @@ This is the **real `@rozie-ui/data-table-vue` package** running on this page (Vi
7382

7483
Each `v-model:<slice>` is a two-way bind — the readout updates the instant you change the state, and a consumer write flows back in. The four slices bound here (`sorting`, `globalFilter`, `rowSelection`, `pagination`) are four of the [nine independent state slices](/components/data-table#models-the-nine-two-way-slices); bind a slice only when you want to own it. The header buttons drive the imperative handle (`toggleAllRows`, `clearSelection`, `clearSorting`) grabbed through Vue's `ref`. A single `#cell` slot on `<DataTable>`, dispatched by `columnId`, renders the **Status** badge; every other column falls through to the plain accessor value (the fast path). See the [full API](/components/data-table) for every prop, slice, event, slot, and handle verb, plus the `<Column>` API, theming, and accessibility reference.
7584

85+
## Row windowing (virtualization)
86+
87+
The same real `@rozie-ui/data-table-vue` package, now over **50,000 rows** with `virtual` + `maxHeight="400px"`. Only the visible slice renders inside the bounded scroll container — scroll the table below and watch the row count stay tiny while the scrollbar spans the full 50,000-row height. Row windowing is GA on all six targets and [**tested to 100,000 rows**](/components/data-table-comparison#feature-matrix) by a DOM/behavioral VR matrix; the default `virtual="false"` is byte-identical to a non-virtual table.
88+
89+
<ClientOnly>
90+
<div class="dt-live">
91+
92+
<DataTable :data="BIG_ROWS" virtual max-height="400px">
93+
<Column field="name" header="Name" />
94+
<Column field="email" header="Email" />
95+
<Column field="status" header="Status" />
96+
</DataTable>
97+
98+
</div>
99+
</ClientOnly>
100+
101+
Set `virtual` to opt in; bound `maxHeight` (or the `--rozie-data-table-max-height` CSS custom property — the prop wins, the token is the fallback) sizes the scroll container, and `estimateRowHeight` seeds the row estimate before `measureElement` refines actual heights. Windowing runs over the full filtered + sorted (pre-pagination) model and suppresses the client pagination chrome. See the [comparison page](/components/data-table-comparison#what-rozie-defers) for the published support boundary (and the orthogonal pieces — column virtualization + dynamic auto-measure — that remain deferred).
102+
76103
## One source, six outputs
77104

78105
You author the component **once** as a `.rozie` file (the parent `DataTable.rozie` plus the declarative `Column.rozie` child):

docs/components/data-table-usage.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,193 @@ render(html`
336336

337337
:::
338338

339+
### Virtualized rows (windowing)
340+
341+
::: code-group
342+
343+
```tsx [React]
344+
import { DataTable, Column } from '@rozie-ui/data-table-react';
345+
346+
// PROP form — bound `maxHeight` sizes the scroll container.
347+
export function Demo() {
348+
const rows = Array.from({ length: 10_000 }, (_, i) => ({
349+
id: i + 1,
350+
name: `Row ${i + 1}`,
351+
email: `user${i + 1}@example.com`,
352+
status: i % 2 ? 'active' : 'away',
353+
}));
354+
return (
355+
<DataTable data={rows} virtual maxHeight="400px">
356+
<Column field="name" header="Name" />
357+
<Column field="email" header="Email" />
358+
<Column field="status" header="Status" />
359+
</DataTable>
360+
);
361+
}
362+
363+
// TOKEN form — the same bound height via the CSS custom property (the prop wins
364+
// when both are set; the token is the fallback). Tune the row estimate too:
365+
// <DataTable data={rows} virtual estimateRowHeight={48}
366+
// style={{ '--rozie-data-table-max-height': '400px' } as React.CSSProperties} />
367+
```
368+
369+
```vue [Vue]
370+
<script setup lang="ts">
371+
import DataTable, { Column } from '@rozie-ui/data-table-vue';
372+
373+
const rows = Array.from({ length: 10_000 }, (_, i) => ({
374+
id: i + 1,
375+
name: `Row ${i + 1}`,
376+
email: `user${i + 1}@example.com`,
377+
status: i % 2 ? 'active' : 'away',
378+
}));
379+
</script>
380+
381+
<template>
382+
<!-- PROP form — bound :maxHeight sizes the scroll container. -->
383+
<DataTable :data="rows" :virtual="true" maxHeight="400px">
384+
<Column field="name" header="Name" />
385+
<Column field="email" header="Email" />
386+
<Column field="status" header="Status" />
387+
</DataTable>
388+
389+
<!-- TOKEN form — the same height via the CSS custom property (prop wins when
390+
both are set; the token is the fallback). :estimateRowHeight tunes the seed. -->
391+
<DataTable
392+
:data="rows"
393+
:virtual="true"
394+
:estimateRowHeight="48"
395+
style="--rozie-data-table-max-height: 400px"
396+
>
397+
<Column field="name" header="Name" />
398+
<Column field="email" header="Email" />
399+
<Column field="status" header="Status" />
400+
</DataTable>
401+
</template>
402+
```
403+
404+
```svelte [Svelte]
405+
<script lang="ts">
406+
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
407+
408+
const rows = Array.from({ length: 10_000 }, (_, i) => ({
409+
id: i + 1,
410+
name: `Row ${i + 1}`,
411+
email: `user${i + 1}@example.com`,
412+
status: i % 2 ? 'active' : 'away',
413+
}));
414+
</script>
415+
416+
<!-- PROP form — bound maxHeight sizes the scroll container. -->
417+
<DataTable data={rows} virtual maxHeight="400px">
418+
<Column field="name" header="Name" />
419+
<Column field="email" header="Email" />
420+
<Column field="status" header="Status" />
421+
</DataTable>
422+
423+
<!-- TOKEN form — the same height via the CSS custom property (prop wins when both
424+
are set; the token is the fallback). estimateRowHeight tunes the seed. -->
425+
<DataTable data={rows} virtual estimateRowHeight={48} style="--rozie-data-table-max-height: 400px">
426+
<Column field="name" header="Name" />
427+
<Column field="email" header="Email" />
428+
<Column field="status" header="Status" />
429+
</DataTable>
430+
```
431+
432+
```ts [Angular]
433+
import { Component } from '@angular/core';
434+
import { DataTable, Column } from '@rozie-ui/data-table-angular';
435+
436+
@Component({
437+
selector: 'app-demo',
438+
standalone: true,
439+
imports: [DataTable, Column],
440+
template: `
441+
<!-- PROP form — bound [maxHeight] sizes the scroll container. -->
442+
<DataTable [data]="rows" [virtual]="true" maxHeight="400px">
443+
<Column field="name" header="Name" />
444+
<Column field="email" header="Email" />
445+
<Column field="status" header="Status" />
446+
</DataTable>
447+
448+
<!-- TOKEN form — the same height via the CSS custom property (prop wins when
449+
both are set; the token is the fallback). [estimateRowHeight] tunes the seed. -->
450+
<DataTable
451+
[data]="rows"
452+
[virtual]="true"
453+
[estimateRowHeight]="48"
454+
style="--rozie-data-table-max-height: 400px"
455+
>
456+
<Column field="name" header="Name" />
457+
<Column field="email" header="Email" />
458+
<Column field="status" header="Status" />
459+
</DataTable>
460+
`,
461+
})
462+
export class DemoComponent {
463+
rows = Array.from({ length: 10_000 }, (_, i) => ({
464+
id: i + 1,
465+
name: `Row ${i + 1}`,
466+
email: `user${i + 1}@example.com`,
467+
status: i % 2 ? 'active' : 'away',
468+
}));
469+
}
470+
```
471+
472+
```tsx [Solid]
473+
import { DataTable, Column } from '@rozie-ui/data-table-solid';
474+
475+
// PROP form — bound maxHeight sizes the scroll container.
476+
export function Demo() {
477+
const rows = Array.from({ length: 10_000 }, (_, i) => ({
478+
id: i + 1,
479+
name: `Row ${i + 1}`,
480+
email: `user${i + 1}@example.com`,
481+
status: i % 2 ? 'active' : 'away',
482+
}));
483+
return (
484+
<DataTable data={rows} virtual maxHeight="400px">
485+
<Column field="name" header="Name" />
486+
<Column field="email" header="Email" />
487+
<Column field="status" header="Status" />
488+
</DataTable>
489+
);
490+
}
491+
492+
// TOKEN form — the same height via the CSS custom property (the prop wins when
493+
// both are set; the token is the fallback). estimateRowHeight tunes the seed:
494+
// <DataTable data={rows} virtual estimateRowHeight={48}
495+
// style={{ '--rozie-data-table-max-height': '400px' }} />
496+
```
497+
498+
```ts [Lit]
499+
import { html, render } from 'lit';
500+
import '@rozie-ui/data-table-lit';
501+
502+
const rows = Array.from({ length: 10_000 }, (_, i) => ({
503+
id: i + 1,
504+
name: `Row ${i + 1}`,
505+
email: `user${i + 1}@example.com`,
506+
status: i % 2 ? 'active' : 'away',
507+
}));
508+
509+
// PROP form — the `max-height` attribute sizes the scroll container.
510+
render(html`
511+
<rozie-data-table .data=${rows} virtual max-height="400px">
512+
<rozie-column field="name" header="Name"></rozie-column>
513+
<rozie-column field="email" header="Email"></rozie-column>
514+
<rozie-column field="status" header="Status"></rozie-column>
515+
</rozie-data-table>
516+
`, document.body);
517+
518+
// TOKEN form — the same height via the CSS custom property (the prop wins when
519+
// both are set; the token is the fallback). `estimate-row-height` tunes the seed:
520+
// <rozie-data-table .data=${rows} virtual estimate-row-height="48"
521+
// style="--rozie-data-table-max-height: 400px"> … </rozie-data-table>
522+
```
523+
524+
:::
525+
339526
## Imperative handle
340527

341528
Beyond props and events, `DataTable` exposes imperative methods (declared once in the `.rozie` source via `$expose`). Grab a handle through your framework's native ref mechanism and call them directly:

0 commit comments

Comments
 (0)