Skip to content

Commit d8a97f8

Browse files
serpentbladeclaude
andcommitted
docs(data-table): correct cell/header rendering model + multi-example-set usage
The custom cell/header renderer is a SINGLE parent-level <DataTable> scoped slot dispatched by columnId (design decision D-A) — `#cell` ({columnId,column,row,value}) / `#colHeader` ({columnId,column,label}) on Vue & Angular, `cell`/`colHeader` snippets/props on Svelte, render-prop props on React (renderCell/renderColHeader) and Solid (cellSlot/colHeaderSlot), and the `.cell`/`.colHeader` properties on Lit. A <Column> carries metadata only. The docs had inherited the pre-D-A per-<Column> `#cell` form (which the leaves no longer honor) in the showcase, the comparison page, the live demo, and the generated Vue README/usage snippet — the demo was dropping the status badge. Fix all of them to the parent columnId-dispatch model. Also add multi-example-set support to the usage pipeline: each target's USAGE entry may now be an aligned list of {title,lang,code} sets, rendered as one heading + code-group per set in both renderReadme and gen-usage-pages.mjs (backward compatible with the single-snippet shape). DataTable now ships two sets — config-array columns and <Column> children + a custom cell — with parity across all six targets. Regenerated the 6 data-table READMEs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wvxhsB5hcDKD8UadLLSyw
1 parent 9360ed4 commit d8a97f8

12 files changed

Lines changed: 722 additions & 96 deletions

File tree

docs/components/data-table-comparison.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Cell legend: **✅** = documented out-of-the-box · **❌** = not supported / no
4242
| Sticky header ||| ⚠️ ||||`stickyHeader` |
4343
| APG grid keyboard navigation (`role="grid"`) || ⚠️ (PrimeVue) ||| ✅ (AG) ||`interactionMode="grid"` |
4444
| Declarative `<Column>` surface | ⚠️ defs array | ✅ (PrimeVue) | ⚠️ | ⚠️ | ⚠️ ||`<Column>` + `:columns` |
45-
| Per-column cell / header templates |||||||`#cell` / `#header` (React render-prop) |
45+
| Custom cell / header rendering |||||||parent `#cell` / `#colHeader`, `columnId`-dispatched (React/Solid render-prop, Lit property) |
4646
| Server-side (manual) mode |||||||`manual` |
4747
| No per-framework adapter ||||||| ✅ single `table-core` |
4848
| Idiomatic two-way state binding | ⚠️ state+onChange |`v-model` | ⚠️ stores | ⚠️ signal | ⚠️ || ✅ nine `r-model` slices |
@@ -55,7 +55,7 @@ Cell legend: **✅** = documented out-of-the-box · **❌** = not supported / no
5555
- **The same component surface everywhere.** Where TanStack offers hooks (React), composables (Vue), stores (Svelte), and signals (Solid) — four mental models over the *same* core — `@rozie-ui/data-table` is one `<DataTable>` + `<Column>` with the same props, nine two-way slices, events, slots, and imperative handle on all six.
5656
- **No per-framework adapter.** Every leaf wires `@tanstack/table-core` directly; the codegen forbids any `@tanstack/<fw>-table` import. You get the exact engine TanStack uses without the adapter-per-framework maintenance surface — and `table-core` is your peer dependency, so you control its version.
5757
- **Nine independent two-way state slices, controlled *or* uncontrolled.** Sorting, global filter, column filters, pagination, row selection, column visibility / sizing / order / pinning — each is an optional `r-model` you bind only if you want to own it, and each change event fires regardless of binding so you can observe transitions either way.
58-
- **A declarative `<Column>` API with per-column templates** (the PrimeVue-shaped surface) *and* a `:columns` config-array escape hatch, resolved by an id-keyed last-write-wins union — plus per-column `#cell` / `#header` reactive templates (a render-prop on React, the one documented divergence).
58+
- **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).
5959
- **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.
6060
- **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.
6161

@@ -66,7 +66,7 @@ This page concedes where the incumbents are genuinely ahead — that's what keep
6666
- **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.
6767
- **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.
6868
- **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>` + per-column templates / 2 selection slots) is stable and gate-verified across all six targets, but it is younger and less battle-tested than the established libraries.
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.
7070

7171
## Try it
7272

docs/components/data-table-demo.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ This is the **real `@rozie-ui/data-table-vue` package** running on this page (Vi
5151
>
5252
<Column field="name" header="Name" :sortable="true" :filterable="true" />
5353
<Column field="email" header="Email" :sortable="true" />
54-
<Column field="status" header="Status" :sortable="true">
55-
<template #cell="{ value }">
56-
<span class="dt-badge" :class="'dt-badge--' + value">{{ value }}</span>
57-
</template>
58-
</Column>
54+
<Column field="status" header="Status" :sortable="true" />
5955
<Column field="score" header="Score" :sortable="true" />
56+
57+
<!-- One #cell slot on <DataTable>, dispatched by columnId -->
58+
<template #cell="{ columnId, value }">
59+
<span v-if="columnId === 'status'" class="dt-badge" :class="'dt-badge--' + value">{{ value }}</span>
60+
<template v-else>{{ value }}</template>
61+
</template>
6062
</DataTable>
6163

6264
<div class="dt-live__state">
@@ -69,7 +71,7 @@ This is the **real `@rozie-ui/data-table-vue` package** running on this page (Vi
6971
</div>
7072
</ClientOnly>
7173

72-
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`. The **Status** column shows a per-column `#cell` template; the others render 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.
74+
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.
7375

7476
## One source, six outputs
7577

docs/components/data-table-usage.md

Lines changed: 203 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ title: DataTable — usage examples
88

99
`DataTable` ships as six pre-compiled, per-framework packages from a single `.rozie` source — install only the one for your framework (no Rozie toolchain, no build-time compile step). Each carries its engine + framework peers as **peer dependencies**, so you control their versions. The snippets below are the same idiomatic consumption code shown in each package's README; switch the tab to your framework.
1010

11-
## Consume it
11+
## Usage
12+
13+
### Columns as a config array
1214

1315
::: code-group
1416

@@ -44,60 +46,56 @@ export function Demo() {
4446
```vue [Vue]
4547
<script setup lang="ts">
4648
import { ref } from 'vue';
47-
import DataTable, { Column } from '@rozie-ui/data-table-vue';
49+
import DataTable from '@rozie-ui/data-table-vue';
4850
4951
const rows = [
5052
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
5153
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
5254
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
5355
];
56+
const columns = [
57+
{ field: 'name', header: 'Name', sortable: true, filterable: true },
58+
{ field: 'email', header: 'Email' },
59+
{ field: 'status', header: 'Status', sortable: true },
60+
];
5461
const sorting = ref<{ id: string; desc: boolean }[]>([]);
5562
</script>
5663
5764
<template>
58-
<DataTable :data="rows" v-model:sorting="sorting" selection-mode="multiple" sticky-header>
59-
<Column field="name" header="Name" :sortable="true" :filterable="true" />
60-
<Column field="email" header="Email" />
61-
<Column field="status" header="Status" :sortable="true">
62-
<template #cell="{ value }"><span class="badge">{{ value }}</span></template>
63-
</Column>
64-
</DataTable>
65+
<DataTable :data="rows" :columns="columns" v-model:sorting="sorting" selection-mode="multiple" sticky-header />
6566
</template>
6667
```
6768

6869
```svelte [Svelte]
6970
<script lang="ts">
70-
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
71+
import DataTable from '@rozie-ui/data-table-svelte';
7172
7273
const rows = [
7374
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
7475
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
7576
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
7677
];
78+
const columns = [
79+
{ field: 'name', header: 'Name', sortable: true, filterable: true },
80+
{ field: 'email', header: 'Email' },
81+
{ field: 'status', header: 'Status', sortable: true },
82+
];
7783
let sorting = $state<{ id: string; desc: boolean }[]>([]);
7884
</script>
7985
80-
<DataTable data={rows} bind:sorting selectionMode="multiple" stickyHeader>
81-
<Column field="name" header="Name" sortable filterable />
82-
<Column field="email" header="Email" />
83-
<Column field="status" header="Status" sortable />
84-
</DataTable>
86+
<DataTable data={rows} {columns} bind:sorting selectionMode="multiple" stickyHeader />
8587
```
8688

8789
```ts [Angular]
8890
import { Component } from '@angular/core';
89-
import { DataTable, Column } from '@rozie-ui/data-table-angular';
91+
import { DataTable } from '@rozie-ui/data-table-angular';
9092

9193
@Component({
9294
selector: 'app-demo',
9395
standalone: true,
94-
imports: [DataTable, Column],
96+
imports: [DataTable],
9597
template: `
96-
<DataTable [data]="rows" [(sorting)]="sorting" selectionMode="multiple" [stickyHeader]="true">
97-
<Column field="name" header="Name" [sortable]="true" [filterable]="true" />
98-
<Column field="email" header="Email" />
99-
<Column field="status" header="Status" [sortable]="true" />
100-
</DataTable>
98+
<DataTable [data]="rows" [columns]="columns" [(sorting)]="sorting" selectionMode="multiple" [stickyHeader]="true" />
10199
`,
102100
})
103101
export class DemoComponent {
@@ -106,6 +104,11 @@ export class DemoComponent {
106104
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
107105
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
108106
];
107+
columns = [
108+
{ field: 'name', header: 'Name', sortable: true, filterable: true },
109+
{ field: 'email', header: 'Email' },
110+
{ field: 'status', header: 'Status', sortable: true },
111+
];
109112
sorting: { id: string; desc: boolean }[] = [];
110113
}
111114
```
@@ -155,6 +158,184 @@ el.addEventListener('sort-change', (e) => {
155158

156159
:::
157160

161+
### Declarative `<Column>` children + a custom cell
162+
163+
::: code-group
164+
165+
```tsx [React]
166+
import { DataTable, Column } from '@rozie-ui/data-table-react';
167+
168+
export function Demo() {
169+
const rows = [
170+
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
171+
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
172+
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
173+
];
174+
// One cell renderer on <DataTable>, dispatched by columnId — it works the same
175+
// whether columns are declared as <Column> children or via :columns.
176+
return (
177+
<DataTable
178+
data={rows}
179+
selectionMode="multiple"
180+
stickyHeader
181+
renderCell={({ columnId, value }) =>
182+
columnId === 'status' ? <StatusBadge status={value} /> : value
183+
}
184+
>
185+
<Column field="name" header="Name" sortable filterable />
186+
<Column field="email" header="Email" />
187+
<Column field="status" header="Status" sortable />
188+
</DataTable>
189+
);
190+
}
191+
```
192+
193+
```vue [Vue]
194+
<script setup lang="ts">
195+
import { ref } from 'vue';
196+
import DataTable, { Column } from '@rozie-ui/data-table-vue';
197+
198+
const rows = [
199+
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
200+
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
201+
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
202+
];
203+
const sorting = ref<{ id: string; desc: boolean }[]>([]);
204+
</script>
205+
206+
<template>
207+
<DataTable :data="rows" v-model:sorting="sorting" selection-mode="multiple" sticky-header>
208+
<Column field="name" header="Name" :sortable="true" :filterable="true" />
209+
<Column field="email" header="Email" />
210+
<Column field="status" header="Status" :sortable="true" />
211+
212+
<!-- One #cell slot on <DataTable>, dispatched by columnId (works with :columns too) -->
213+
<template #cell="{ columnId, value }">
214+
<span v-if="columnId === 'status'" class="badge">{{ value }}</span>
215+
<template v-else>{{ value }}</template>
216+
</template>
217+
</DataTable>
218+
</template>
219+
```
220+
221+
```svelte [Svelte]
222+
<script lang="ts">
223+
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
224+
225+
const rows = [
226+
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
227+
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
228+
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
229+
];
230+
let sorting = $state<{ id: string; desc: boolean }[]>([]);
231+
</script>
232+
233+
<DataTable data={rows} bind:sorting selectionMode="multiple" stickyHeader>
234+
<Column field="name" header="Name" sortable filterable />
235+
<Column field="email" header="Email" />
236+
<Column field="status" header="Status" sortable />
237+
238+
<!-- One cell snippet on <DataTable>, dispatched by columnId -->
239+
{#snippet cell({ columnId, value })}
240+
{#if columnId === 'status'}<span class="badge">{value}</span>{:else}{value}{/if}
241+
{/snippet}
242+
</DataTable>
243+
```
244+
245+
```ts [Angular]
246+
import { Component } from '@angular/core';
247+
import { DataTable, Column } from '@rozie-ui/data-table-angular';
248+
249+
@Component({
250+
selector: 'app-demo',
251+
standalone: true,
252+
imports: [DataTable, Column],
253+
template: `
254+
<DataTable [data]="rows" [(sorting)]="sorting" selectionMode="multiple" [stickyHeader]="true">
255+
<Column field="name" header="Name" [sortable]="true" [filterable]="true" />
256+
<Column field="email" header="Email" />
257+
<Column field="status" header="Status" [sortable]="true" />
258+
259+
<!-- One #cell template on <DataTable>, dispatched by columnId -->
260+
<ng-template #cell let-columnId="columnId" let-value="value">
261+
@if (columnId === 'status') {
262+
<span class="badge">{{ value }}</span>
263+
} @else {
264+
{{ value }}
265+
}
266+
</ng-template>
267+
</DataTable>
268+
`,
269+
})
270+
export class DemoComponent {
271+
rows = [
272+
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
273+
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
274+
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
275+
];
276+
sorting: { id: string; desc: boolean }[] = [];
277+
}
278+
```
279+
280+
```tsx [Solid]
281+
import { createSignal } from 'solid-js';
282+
import { DataTable, Column } from '@rozie-ui/data-table-solid';
283+
284+
export function Demo() {
285+
const rows = [
286+
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
287+
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
288+
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
289+
];
290+
const [sorting, setSorting] = createSignal<{ id: string; desc: boolean }[]>([]);
291+
return (
292+
<DataTable
293+
data={rows}
294+
sorting={sorting()}
295+
onSortChange={setSorting}
296+
selectionMode="multiple"
297+
stickyHeader
298+
cellSlot={({ columnId, value }) =>
299+
columnId === 'status' ? <StatusBadge status={value} /> : value
300+
}
301+
>
302+
<Column field="name" header="Name" sortable filterable />
303+
<Column field="email" header="Email" />
304+
<Column field="status" header="Status" sortable />
305+
</DataTable>
306+
);
307+
}
308+
```
309+
310+
```ts [Lit]
311+
import { html, render } from 'lit';
312+
import '@rozie-ui/data-table-lit';
313+
314+
const rows = [
315+
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
316+
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
317+
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
318+
];
319+
320+
// Declare columns as <rozie-column> children and supply ONE cell renderer —
321+
// a function returning a Lit template, dispatched by columnId.
322+
render(html`
323+
<rozie-data-table
324+
.data=${rows}
325+
selection-mode="multiple"
326+
sticky-header
327+
.cell=${({ columnId, value }) =>
328+
columnId === 'status' ? html`<span class="badge">${value}</span>` : value}
329+
>
330+
<rozie-column field="name" header="Name" sortable filterable></rozie-column>
331+
<rozie-column field="email" header="Email"></rozie-column>
332+
<rozie-column field="status" header="Status" sortable></rozie-column>
333+
</rozie-data-table>
334+
`, document.body);
335+
```
336+
337+
:::
338+
158339
## Imperative handle
159340

160341
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)