Appearance
DataTable — usage examples
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.
Usage
Columns as a config array
tsx
import { useState } from 'react';
import { DataTable } from '@rozie-ui/data-table-react';
export function Demo() {
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
const columns = [
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email' },
{ field: 'status', header: 'Status', sortable: true },
];
const [sorting, setSorting] = useState<{ id: string; desc: boolean }[]>([]);
return (
<DataTable
data={rows}
columns={columns}
sorting={sorting}
onSortChange={setSorting}
selectionMode="multiple"
stickyHeader
/>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable from '@rozie-ui/data-table-vue';
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
const columns = [
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email' },
{ field: 'status', header: 'Status', sortable: true },
];
const sorting = ref<{ id: string; desc: boolean }[]>([]);
</script>
<template>
<DataTable :data="rows" :columns="columns" v-model:sorting="sorting" selection-mode="multiple" sticky-header />
</template>svelte
<script lang="ts">
import DataTable from '@rozie-ui/data-table-svelte';
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
const columns = [
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email' },
{ field: 'status', header: 'Status', sortable: true },
];
let sorting = $state<{ id: string; desc: boolean }[]>([]);
</script>
<DataTable data={rows} {columns} bind:sorting selectionMode="multiple" stickyHeader />ts
import { Component } from '@angular/core';
import { DataTable } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable],
template: `
<DataTable [data]="rows" [columns]="columns" [(sorting)]="sorting" selectionMode="multiple" [stickyHeader]="true" />
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
columns = [
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email' },
{ field: 'status', header: 'Status', sortable: true },
];
sorting: { id: string; desc: boolean }[] = [];
}tsx
import { createSignal } from 'solid-js';
import { DataTable } from '@rozie-ui/data-table-solid';
export function Demo() {
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
const columns = [
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email' },
{ field: 'status', header: 'Status', sortable: true },
];
const [sorting, setSorting] = createSignal<{ id: string; desc: boolean }[]>([]);
return (
<DataTable data={rows} columns={columns} sorting={sorting()} onSortChange={setSorting} selectionMode="multiple" stickyHeader />
);
}ts
import '@rozie-ui/data-table-lit';
// <rozie-data-table> is a custom element. Set `data`/`columns` as properties
// and listen for the change events (`sort-change`, `filter-change`, …).
const el = document.querySelector('rozie-data-table');
el.data = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
el.columns = [
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email' },
{ field: 'status', header: 'Status', sortable: true },
];
el.addEventListener('sort-change', (e) => {
console.log('sorting', e.detail);
});Declarative <Column> children + a custom cell
tsx
import { DataTable, Column } from '@rozie-ui/data-table-react';
export function Demo() {
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
// One cell renderer on <DataTable>, dispatched by columnId — it works the same
// whether columns are declared as <Column> children or via :columns.
return (
<DataTable
data={rows}
selectionMode="multiple"
stickyHeader
renderCell={({ columnId, value }) =>
columnId === 'status' ? <StatusBadge status={value} /> : value
}
>
<Column field="name" header="Name" sortable filterable />
<Column field="email" header="Email" />
<Column field="status" header="Status" sortable />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, { Column } from '@rozie-ui/data-table-vue';
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
const sorting = ref<{ id: string; desc: boolean }[]>([]);
</script>
<template>
<DataTable :data="rows" v-model:sorting="sorting" selection-mode="multiple" sticky-header>
<Column field="name" header="Name" :sortable="true" :filterable="true" />
<Column field="email" header="Email" />
<Column field="status" header="Status" :sortable="true" />
<!-- One #cell slot on <DataTable>, dispatched by columnId (works with :columns too) -->
<template #cell="{ columnId, value }">
<span v-if="columnId === 'status'" class="badge">{{ value }}</span>
<template v-else>{{ value }}</template>
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
let sorting = $state<{ id: string; desc: boolean }[]>([]);
</script>
<DataTable data={rows} bind:sorting selectionMode="multiple" stickyHeader>
<Column field="name" header="Name" sortable filterable />
<Column field="email" header="Email" />
<Column field="status" header="Status" sortable />
<!-- One cell snippet on <DataTable>, dispatched by columnId -->
{#snippet cell({ columnId, value })}
{#if columnId === 'status'}<span class="badge">{value}</span>{:else}{value}{/if}
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import { DataTable, Column } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column],
template: `
<DataTable [data]="rows" [(sorting)]="sorting" selectionMode="multiple" [stickyHeader]="true">
<Column field="name" header="Name" [sortable]="true" [filterable]="true" />
<Column field="email" header="Email" />
<Column field="status" header="Status" [sortable]="true" />
<!-- One #cell template on <DataTable>, dispatched by columnId -->
<ng-template #cell let-columnId="columnId" let-value="value">
@if (columnId === 'status') {
<span class="badge">{{ value }}</span>
} @else {
{{ value }}
}
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
sorting: { id: string; desc: boolean }[] = [];
}tsx
import { createSignal } from 'solid-js';
import { DataTable, Column } from '@rozie-ui/data-table-solid';
export function Demo() {
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
const [sorting, setSorting] = createSignal<{ id: string; desc: boolean }[]>([]);
return (
<DataTable
data={rows}
sorting={sorting()}
onSortChange={setSorting}
selectionMode="multiple"
stickyHeader
cellSlot={({ columnId, value }) =>
columnId === 'status' ? <StatusBadge status={value} /> : value
}
>
<Column field="name" header="Name" sortable filterable />
<Column field="email" header="Email" />
<Column field="status" header="Status" sortable />
</DataTable>
);
}ts
import { html, render } from 'lit';
import '@rozie-ui/data-table-lit';
const rows = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', status: 'active' },
{ id: 2, name: 'Alan Turing', email: 'alan@bletchley.park', status: 'active' },
{ id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', status: 'away' },
];
// Declare columns as <rozie-column> children and supply ONE cell renderer —
// a function returning a Lit template, dispatched by columnId.
render(html`
<rozie-data-table
.data=${rows}
selection-mode="multiple"
sticky-header
.cell=${({ columnId, value }) =>
columnId === 'status' ? html`<span class="badge">${value}</span>` : value}
>
<rozie-column field="name" header="Name" sortable filterable></rozie-column>
<rozie-column field="email" header="Email"></rozie-column>
<rozie-column field="status" header="Status" sortable></rozie-column>
</rozie-data-table>
`, document.body);Virtualized rows (windowing)
tsx
import { DataTable, Column } from '@rozie-ui/data-table-react';
// PROP form — bound `maxHeight` sizes the scroll container.
export function Demo() {
const rows = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `Row ${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2 ? 'active' : 'away',
}));
return (
<DataTable data={rows} virtual maxHeight="400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
);
}
// TOKEN form — the same bound height via the CSS custom property (the prop wins
// when both are set; the token is the fallback). Tune the row estimate too:
// <DataTable data={rows} virtual estimateRowHeight={48}
// style={{ '--rozie-data-table-max-height': '400px' } as React.CSSProperties} />vue
<script setup lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-vue';
const rows = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `Row ${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2 ? 'active' : 'away',
}));
</script>
<template>
<!-- PROP form — bound :maxHeight sizes the scroll container. -->
<DataTable :data="rows" :virtual="true" maxHeight="400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
<!-- TOKEN form — the same height via the CSS custom property (prop wins when
both are set; the token is the fallback). :estimateRowHeight tunes the seed. -->
<DataTable
:data="rows"
:virtual="true"
:estimateRowHeight="48"
style="--rozie-data-table-max-height: 400px"
>
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
const rows = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `Row ${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2 ? 'active' : 'away',
}));
</script>
<!-- PROP form — bound maxHeight sizes the scroll container. -->
<DataTable data={rows} virtual maxHeight="400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
<!-- TOKEN form — the same height via the CSS custom property (prop wins when both
are set; the token is the fallback). estimateRowHeight tunes the seed. -->
<DataTable data={rows} virtual estimateRowHeight={48} style="--rozie-data-table-max-height: 400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>ts
import { Component } from '@angular/core';
import { DataTable, Column } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column],
template: `
<!-- PROP form — bound [maxHeight] sizes the scroll container. -->
<DataTable [data]="rows" [virtual]="true" maxHeight="400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
<!-- TOKEN form — the same height via the CSS custom property (prop wins when
both are set; the token is the fallback). [estimateRowHeight] tunes the seed. -->
<DataTable
[data]="rows"
[virtual]="true"
[estimateRowHeight]="48"
style="--rozie-data-table-max-height: 400px"
>
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
`,
})
export class DemoComponent {
rows = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `Row ${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2 ? 'active' : 'away',
}));
}tsx
import { DataTable, Column } from '@rozie-ui/data-table-solid';
// PROP form — bound maxHeight sizes the scroll container.
export function Demo() {
const rows = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `Row ${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2 ? 'active' : 'away',
}));
return (
<DataTable data={rows} virtual maxHeight="400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column field="status" header="Status" />
</DataTable>
);
}
// TOKEN form — the same height via the CSS custom property (the prop wins when
// both are set; the token is the fallback). estimateRowHeight tunes the seed:
// <DataTable data={rows} virtual estimateRowHeight={48}
// style={{ '--rozie-data-table-max-height': '400px' }} />ts
import { html, render } from 'lit';
import '@rozie-ui/data-table-lit';
const rows = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `Row ${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2 ? 'active' : 'away',
}));
// PROP form — the `max-height` attribute sizes the scroll container.
render(html`
<rozie-data-table .data=${rows} virtual max-height="400px">
<rozie-column field="name" header="Name"></rozie-column>
<rozie-column field="email" header="Email"></rozie-column>
<rozie-column field="status" header="Status"></rozie-column>
</rozie-data-table>
`, document.body);
// TOKEN form — the same height via the CSS custom property (the prop wins when
// both are set; the token is the fallback). `estimate-row-height` tunes the seed:
// <rozie-data-table .data=${rows} virtual estimate-row-height="48"
// style="--rozie-data-table-max-height: 400px"> … </rozie-data-table>Editable cells (inline edit + validation)
tsx
import { useState } from 'react';
import { DataTable, Column } from '@rozie-ui/data-table-react';
export function Demo() {
// The component OWNS edit state — bind ONE model ('data') + listen for commits.
const [rows, setRows] = useState([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
return (
<DataTable
interactionMode="grid"
data={rows}
onDataChange={setRows}
onCellEditCommit={({ rowId, columnId, oldValue, newValue }) =>
console.log('cell commit', rowId, columnId, oldValue, '→', newValue)
}
onRowEditCommit={({ rowId, changes }) => console.log('row commit', rowId, changes)}
// The #editor scoped slot is a render prop on React (the documented edge).
renderEditor={({ columnId, value, commit, cancel }) =>
columnId === 'score' ? (
<span>
<button onClick={() => commit(Number(value) - 1)}>−</button>
<button onClick={() => commit(Number(value) + 1)}>+</button>
<button onClick={cancel}>esc</button>
</span>
) : null
}
>
<Column field="name" header="Name" editable editor="text" />
<Column field="qty" header="Qty" editable editor="number"
validate={(value) => Number(value) >= 0 || 'must be >= 0'} />
<Column field="status" header="Status" editable editor="select" editorOptions={[
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
]} />
<Column field="active" header="Active" editable editor="checkbox" />
<Column field="score" header="Score" editable editor="custom" />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, { Column } from '@rozie-ui/data-table-vue';
// The component OWNS edit state — bind ONE model (v-model:data) + listen for commits.
const rows = ref([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
const validateQty = (value: unknown) => Number(value) >= 0 || 'must be >= 0';
</script>
<template>
<DataTable
interaction-mode="grid"
v-model:data="rows"
@cell-edit-commit="(p) => console.log('cell commit', p)"
@row-edit-commit="(p) => console.log('row commit', p)"
>
<Column field="name" header="Name" :editable="true" editor="text" />
<Column field="qty" header="Qty" :editable="true" editor="number" :validate="validateQty" />
<Column field="status" header="Status" :editable="true" editor="select" :editorOptions="statusOptions" />
<Column field="active" header="Active" :editable="true" editor="checkbox" />
<Column field="score" header="Score" :editable="true" editor="custom" />
<!-- The #editor scoped slot replaces the built-in editor for one column. -->
<template #editor="{ columnId, value, commit, cancel }">
<span v-if="columnId === 'score'">
<button type="button" @click="commit(Number(value) - 1)">−</button>
<button type="button" @click="commit(Number(value) + 1)">+</button>
<button type="button" @click="cancel()">esc</button>
</span>
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
// The component OWNS edit state — bind ONE model (bind:data) + listen for commits.
let rows = $state([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
const validateQty = (value: unknown) => Number(value) >= 0 || 'must be >= 0';
</script>
<DataTable
interactionMode="grid"
bind:data={rows}
oncelleditcommit={(p) => console.log('cell commit', p)}
onroweditcommit={(p) => console.log('row commit', p)}
>
<Column field="name" header="Name" editable editor="text" />
<Column field="qty" header="Qty" editable editor="number" validate={validateQty} />
<Column field="status" header="Status" editable editor="select" editorOptions={statusOptions} />
<Column field="active" header="Active" editable editor="checkbox" />
<Column field="score" header="Score" editable editor="custom" />
<!-- The #editor scoped slot is a snippet on Svelte; it replaces the built-in editor. -->
{#snippet editor({ columnId, value, commit, cancel })}
{#if columnId === 'score'}
<span>
<button type="button" onclick={() => commit(Number(value) - 1)}>−</button>
<button type="button" onclick={() => commit(Number(value) + 1)}>+</button>
<button type="button" onclick={() => cancel()}>esc</button>
</span>
{/if}
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import { DataTable, Column } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column],
template: `
<!-- The component OWNS edit state — bind ONE model [(data)] + listen for commits. -->
<DataTable
interactionMode="grid"
[(data)]="rows"
(cell-edit-commit)="onCellCommit($event)"
(row-edit-commit)="onRowCommit($event)"
>
<Column field="name" header="Name" [editable]="true" editor="text" />
<Column field="qty" header="Qty" [editable]="true" editor="number" [validate]="validateQty" />
<Column field="status" header="Status" [editable]="true" editor="select" [editorOptions]="statusOptions" />
<Column field="active" header="Active" [editable]="true" editor="checkbox" />
<Column field="score" header="Score" [editable]="true" editor="custom" />
<!-- The #editor scoped slot is an ng-template; it replaces the built-in editor. -->
<ng-template #editor let-columnId="columnId" let-value="value" let-commit="commit" let-cancel="cancel">
@if (columnId === 'score') {
<span>
<button type="button" (click)="commit(+value - 1)">−</button>
<button type="button" (click)="commit(+value + 1)">+</button>
<button type="button" (click)="cancel()">esc</button>
</span>
}
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
];
statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
validateQty = (value: unknown) => Number(value) >= 0 || 'must be >= 0';
onCellCommit(p: unknown) { console.log('cell commit', p); }
onRowCommit(p: unknown) { console.log('row commit', p); }
}tsx
import { createSignal } from 'solid-js';
import { DataTable, Column } from '@rozie-ui/data-table-solid';
export function Demo() {
// The component OWNS edit state — bind ONE model ('data') + listen for commits.
const [rows, setRows] = createSignal([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
return (
<DataTable
interactionMode="grid"
data={rows()}
onDataChange={setRows}
onCellEditCommit={(p) => console.log('cell commit', p)}
onRowEditCommit={(p) => console.log('row commit', p)}
// The #editor scoped slot is a render prop on Solid (the documented edge).
editorSlot={({ columnId, value, commit, cancel }) =>
columnId === 'score' ? (
<span>
<button onClick={() => commit(Number(value) - 1)}>−</button>
<button onClick={() => commit(Number(value) + 1)}>+</button>
<button onClick={() => cancel()}>esc</button>
</span>
) : null
}
>
<Column field="name" header="Name" editable editor="text" />
<Column field="qty" header="Qty" editable editor="number"
validate={(value) => Number(value) >= 0 || 'must be >= 0'} />
<Column field="status" header="Status" editable editor="select" editorOptions={statusOptions} />
<Column field="active" header="Active" editable editor="checkbox" />
<Column field="score" header="Score" editable editor="custom" />
</DataTable>
);
}ts
import { html, render } from 'lit';
import '@rozie-ui/data-table-lit';
// The component OWNS edit state — set the `data` property + listen for commits.
let rows = [
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
];
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
const validateQty = (value: unknown) => Number(value) >= 0 || 'must be >= 0';
render(html`
<rozie-data-table
interaction-mode="grid"
.data=${rows}
@data-change=${(e: CustomEvent) => { rows = e.detail; }}
@cell-edit-commit=${(e: CustomEvent) => console.log('cell commit', e.detail)}
@row-edit-commit=${(e: CustomEvent) => console.log('row commit', e.detail)}
.editor=${({ columnId, value, commit, cancel }) =>
columnId === 'score'
? html`<span>
<button @click=${() => commit(Number(value) - 1)}>−</button>
<button @click=${() => commit(Number(value) + 1)}>+</button>
<button @click=${() => cancel()}>esc</button>
</span>`
: null}
>
<rozie-column field="name" header="Name" editable editor="text"></rozie-column>
<rozie-column field="qty" header="Qty" editable editor="number" .validate=${validateQty}></rozie-column>
<rozie-column field="status" header="Status" editable editor="select" .editorOptions=${statusOptions}></rozie-column>
<rozie-column field="active" header="Active" editable editor="checkbox"></rozie-column>
<rozie-column field="score" header="Score" editable editor="custom"></rozie-column>
</rozie-data-table>
`, document.body);Expandable rows (#detail slot + nested sub-rows)
tsx
import { useState } from 'react';
import { DataTable, Column } from '@rozie-ui/data-table-react';
export function Demo() {
// `expandable` opts the table into getExpandedRowModel + the auto-injected chevron
// column. `getSubRows` yields depth-indented child rows; the #detail slot renders an
// arbitrary panel under any open row. The two-way `expanded` set keeps MULTIPLE rows open.
const rows = [
{ id: 1, name: 'Engineering', headcount: 12, children: [
{ id: 11, name: 'Frontend', headcount: 5 },
{ id: 12, name: 'Backend', headcount: 7 },
] },
{ id: 2, name: 'Sales', headcount: 8 },
];
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
return (
<DataTable
data={rows}
expandable
expanded={expanded}
onExpandChange={setExpanded} // the event is `expand-change` → onExpandChange
getSubRows={(row) => row.children} // depth-indented nested rows (pattern b)
// The #detail scoped slot is a render prop on React (the documented edge).
renderDetail={({ row }) => <aside className="detail">More about {row.name}</aside>}
>
<Column field="name" header="Name" />
<Column field="headcount" header="Headcount" />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, { Column } from '@rozie-ui/data-table-vue';
const rows = [
{ id: 1, name: 'Engineering', headcount: 12, children: [
{ id: 11, name: 'Frontend', headcount: 5 },
{ id: 12, name: 'Backend', headcount: 7 },
] },
{ id: 2, name: 'Sales', headcount: 8 },
];
const expanded = ref<Record<string, boolean>>({});
const getSubRows = (row: { children?: unknown[] }) => row.children;
</script>
<template>
<!-- expandable opts in; v-model:expanded keeps MULTIPLE rows open; getSubRows yields
depth-indented child rows; the #detail scoped slot renders a panel under each open row. -->
<DataTable :data="rows" :expandable="true" v-model:expanded="expanded" :getSubRows="getSubRows">
<Column field="name" header="Name" />
<Column field="headcount" header="Headcount" />
<template #detail="{ row }">
<aside class="detail">More about {{ row.name }}</aside>
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
const rows = [
{ id: 1, name: 'Engineering', headcount: 12, children: [
{ id: 11, name: 'Frontend', headcount: 5 },
{ id: 12, name: 'Backend', headcount: 7 },
] },
{ id: 2, name: 'Sales', headcount: 8 },
];
// bind:expanded keeps MULTIPLE rows open; getSubRows yields depth-indented child rows.
let expanded = $state<Record<string, boolean>>({});
const getSubRows = (row: { children?: unknown[] }) => row.children;
</script>
<DataTable data={rows} expandable bind:expanded {getSubRows}>
<Column field="name" header="Name" />
<Column field="headcount" header="Headcount" />
<!-- The #detail scoped slot is a snippet on Svelte; it renders under each open row. -->
{#snippet detail({ row })}
<aside class="detail">More about {row.name}</aside>
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import { DataTable, Column } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column],
template: `
<!-- expandable opts in; [(expanded)] keeps MULTIPLE rows open; getSubRows yields nested rows. -->
<DataTable [data]="rows" [expandable]="true" [(expanded)]="expanded" [getSubRows]="getSubRows">
<Column field="name" header="Name" />
<Column field="headcount" header="Headcount" />
<!-- The #detail scoped slot is an ng-template receiving { row }. -->
<ng-template #detail let-row="row">
<aside class="detail">More about {{ row.name }}</aside>
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Engineering', headcount: 12, children: [
{ id: 11, name: 'Frontend', headcount: 5 },
{ id: 12, name: 'Backend', headcount: 7 },
] },
{ id: 2, name: 'Sales', headcount: 8 },
];
expanded: Record<string, boolean> = {};
getSubRows = (row: { children?: unknown[] }) => row.children;
}tsx
import { createSignal } from 'solid-js';
import { DataTable, Column } from '@rozie-ui/data-table-solid';
export function Demo() {
// expandable opts in; the two-way `expanded` set keeps MULTIPLE rows open; getSubRows
// yields depth-indented child rows; the #detail slot renders a panel under any open row.
const rows = [
{ id: 1, name: 'Engineering', headcount: 12, children: [
{ id: 11, name: 'Frontend', headcount: 5 },
{ id: 12, name: 'Backend', headcount: 7 },
] },
{ id: 2, name: 'Sales', headcount: 8 },
];
const [expanded, setExpanded] = createSignal<Record<string, boolean>>({});
return (
<DataTable
data={rows}
expandable
expanded={expanded()}
onExpandChange={setExpanded} // the event is `expand-change` → onExpandChange
getSubRows={(row) => row.children} // depth-indented nested rows
// The #detail scoped slot is a render prop on Solid (the documented edge).
detailSlot={({ row }) => <aside class="detail">More about {row.name}</aside>}
>
<Column field="name" header="Name" />
<Column field="headcount" header="Headcount" />
</DataTable>
);
}ts
import { html, render } from 'lit';
import '@rozie-ui/data-table-lit';
const rows = [
{ id: 1, name: 'Engineering', headcount: 12, children: [
{ id: 11, name: 'Frontend', headcount: 5 },
{ id: 12, name: 'Backend', headcount: 7 },
] },
{ id: 2, name: 'Sales', headcount: 8 },
];
// expandable opts in; listen for `expand-change`; getSubRows yields depth-indented nested
// rows; the #detail scoped slot is the `.detail` property (a function returning a template).
render(html`
<rozie-data-table
.data=${rows}
expandable
.getSubRows=${(row: { children?: unknown[] }) => row.children}
.detail=${({ row }) => html`<aside class="detail">More about ${row.name}</aside>`}
@expand-change=${(e: CustomEvent) => console.log('expanded', e.detail)}
>
<rozie-column field="name" header="Name"></rozie-column>
<rozie-column field="headcount" header="Headcount"></rozie-column>
</rozie-data-table>
`, document.body);Grouping + aggregation (headless #groupBar)
tsx
import { useState } from 'react';
import { DataTable, Column } from '@rozie-ui/data-table-react';
export function Demo() {
// `groupable` enables getGroupedRowModel. The `grouping` model is an ORDERED column-id
// list (multi-column → nested groups). Per-column `aggregationFn` rolls leaf values up
// into the group-header row (a built-in name OR a custom fn). #groupBar is HEADLESS —
// you build the bar from its props; the component ships NO drag UI (D-02 retired).
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
const [grouping, setGrouping] = useState<string[]>([]);
const scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
const v = leafRows.map((r) => Number(r.getValue(columnId)));
return v.length ? Math.max(...v) - Math.min(...v) : 0;
};
return (
<DataTable
data={rows}
groupable
grouping={grouping}
onGroupChange={setGrouping} // the event is `group-change` → onGroupChange
// The #groupBar scoped slot is a render prop on React (the documented edge).
renderGroupBar={({ grouping, groupableColumns, applyGrouping, clearGrouping }) => (
<div>
<button onClick={() => applyGrouping(['region', 'category'])}>Group region → category</button>
<button onClick={() => clearGrouping()}>Clear</button>
<span>{grouping.join(' → ') || 'ungrouped'} ({groupableColumns.length} groupable)</span>
</div>
)}
>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<Column field="score" header="Score" aggregationFn={scoreRange} />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, { Column } from '@rozie-ui/data-table-vue';
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
const grouping = ref<string[]>([]);
// A custom per-column aggregation (range = max − min) over the group's leaf rows.
const scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
const v = leafRows.map((r) => Number(r.getValue(columnId)));
return v.length ? Math.max(...v) - Math.min(...v) : 0;
};
</script>
<template>
<!-- groupable enables grouping; the model is an ORDERED column-id list; aggregationFn
rolls leaf values into the group header. The event is `group-change`. -->
<DataTable :data="rows" :groupable="true" v-model:grouping="grouping" @group-change="(g) => console.log('grouping', g)">
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<Column field="score" header="Score" :aggregationFn="scoreRange" />
<!-- #groupBar is HEADLESS — build the bar from its props (NO built-in drag UI). -->
<template #groupBar="{ grouping, groupableColumns, applyGrouping, clearGrouping }">
<div class="group-bar">
<button type="button" @click="applyGrouping(['region', 'category'])">Group region → category</button>
<button type="button" @click="clearGrouping()">Clear</button>
<span>{{ grouping.join(' → ') || 'ungrouped' }} ({{ groupableColumns.length }} groupable)</span>
</div>
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
let grouping = $state<string[]>([]);
const scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
const v = leafRows.map((r) => Number(r.getValue(columnId)));
return v.length ? Math.max(...v) - Math.min(...v) : 0;
};
</script>
<!-- groupable enables grouping; the model is an ORDERED column-id list; the event is
`group-change` → ongroupchange. aggregationFn rolls leaf values into the group header. -->
<DataTable data={rows} groupable bind:grouping ongroupchange={(g) => console.log('grouping', g)}>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<Column field="score" header="Score" aggregationFn={scoreRange} />
<!-- #groupBar is HEADLESS — build the bar from its props (NO built-in drag UI). -->
{#snippet groupBar({ grouping, groupableColumns, applyGrouping, clearGrouping })}
<div class="group-bar">
<button type="button" onclick={() => applyGrouping(['region', 'category'])}>Group region → category</button>
<button type="button" onclick={() => clearGrouping()}>Clear</button>
<span>{grouping.join(' → ') || 'ungrouped'} ({groupableColumns.length} groupable)</span>
</div>
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import { DataTable, Column } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column],
template: `
<!-- groupable enables grouping; [(grouping)] is an ORDERED column-id list; the event
is `group-change`. aggregationFn rolls leaf values into the group header. -->
<DataTable [data]="rows" [groupable]="true" [(grouping)]="grouping" (group-change)="onGroupChange($event)">
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<Column field="score" header="Score" [aggregationFn]="scoreRange" />
<!-- #groupBar is HEADLESS — build the bar from its props (NO built-in drag UI). -->
<ng-template #groupBar let-grouping="grouping" let-groupableColumns="groupableColumns" let-applyGrouping="applyGrouping" let-clearGrouping="clearGrouping">
<div class="group-bar">
<button type="button" (click)="applyGrouping(['region', 'category'])">Group region → category</button>
<button type="button" (click)="clearGrouping()">Clear</button>
<span>{{ grouping.join(' → ') || 'ungrouped' }} ({{ groupableColumns.length }} groupable)</span>
</div>
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
grouping: string[] = [];
scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
const v = leafRows.map((r) => Number(r.getValue(columnId)));
return v.length ? Math.max(...v) - Math.min(...v) : 0;
};
onGroupChange(g: string[]) { console.log('grouping', g); }
}tsx
import { createSignal } from 'solid-js';
import { DataTable, Column } from '@rozie-ui/data-table-solid';
export function Demo() {
// groupable enables grouping; the `grouping` model is an ORDERED column-id list;
// aggregationFn rolls leaf values into the group header. #groupBar is HEADLESS (no drag).
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
const [grouping, setGrouping] = createSignal<string[]>([]);
const scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
const v = leafRows.map((r) => Number(r.getValue(columnId)));
return v.length ? Math.max(...v) - Math.min(...v) : 0;
};
return (
<DataTable
data={rows}
groupable
grouping={grouping()}
onGroupChange={setGrouping} // the event is `group-change` → onGroupChange
// The #groupBar scoped slot is a render prop on Solid (the documented edge).
groupBarSlot={({ grouping, groupableColumns, applyGrouping, clearGrouping }) => (
<div>
<button onClick={() => applyGrouping(['region', 'category'])}>Group region → category</button>
<button onClick={() => clearGrouping()}>Clear</button>
<span>{grouping.join(' → ') || 'ungrouped'} ({groupableColumns.length} groupable)</span>
</div>
)}
>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<Column field="score" header="Score" aggregationFn={scoreRange} />
</DataTable>
);
}ts
import { html, render } from 'lit';
import '@rozie-ui/data-table-lit';
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
const scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
const v = leafRows.map((r) => Number(r.getValue(columnId)));
return v.length ? Math.max(...v) - Math.min(...v) : 0;
};
// groupable enables grouping; listen for `group-change`; #groupBar is the headless
// `.groupBar` property (NO built-in drag UI) — build the bar from its props.
render(html`
<rozie-data-table
.data=${rows}
groupable
@group-change=${(e: CustomEvent) => console.log('grouping', e.detail)}
.groupBar=${({ grouping, groupableColumns, applyGrouping, clearGrouping }) => html`
<div>
<button @click=${() => applyGrouping(['region', 'category'])}>Group region → category</button>
<button @click=${() => clearGrouping()}>Clear</button>
<span>${grouping.join(' → ') || 'ungrouped'} (${groupableColumns.length} groupable)</span>
</div>`}
>
<rozie-column field="region" header="Region"></rozie-column>
<rozie-column field="category" header="Category"></rozie-column>
<rozie-column field="units" header="Units" .aggregationFn=${'sum'}></rozie-column>
<rozie-column field="score" header="Score" .aggregationFn=${scoreRange}></rozie-column>
</rozie-data-table>
`, document.body);Faceted filtering exposure (headless #filter)
tsx
import { useRef, useState } from 'react';
import { DataTable, Column, type DataTableHandle } from '@rozie-ui/data-table-react';
export function Demo() {
// Faceting is HEADLESS + read-only: NO event, NO built-in facet control. The #filter
// scoped slot hands you the cross-filtered `uniqueValues` (keys only) + numeric `minMax`;
// you build the checkbox list / range slider and drive `columnFilters`. The
// getFacetedUniqueValues / getFacetedMinMaxValues handle verbs read the same data back.
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
const [columnFilters, setColumnFilters] = useState<{ id: string; value: unknown }[]>([]);
const tbl = useRef<DataTableHandle>(null);
return (
<DataTable
ref={tbl}
data={rows}
columnFilters={columnFilters}
onFilterChange={(p) => p.columnFilters && setColumnFilters(p.columnFilters)}
// The #filter scoped slot is a render prop on React (the documented edge).
renderFilter={({ columnId, uniqueValues, minMax }) =>
columnId === 'category' ? (
<fieldset>
{uniqueValues.map((v) => (
<label key={String(v)}><input type="checkbox" /> {String(v)}</label>
))}
</fieldset>
) : (
<input type="range" min={minMax?.[0]} max={minMax?.[1]} />
)
}
>
<Column field="name" header="Name" />
<Column field="category" header="Category" filterable />
<Column field="price" header="Price" filterable />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, { Column } from '@rozie-ui/data-table-vue';
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
const columnFilters = ref<{ id: string; value: unknown }[]>([]);
</script>
<template>
<!-- Faceting is HEADLESS + read-only (NO event, NO built-in control). The #filter slot
hands you `uniqueValues` (keys, cross-filtered) + numeric `minMax`; build the UI and
drive v-model:columnFilters. -->
<DataTable :data="rows" v-model:columnFilters="columnFilters">
<Column field="name" header="Name" />
<Column field="category" header="Category" :filterable="true" />
<Column field="price" header="Price" :filterable="true" />
<template #filter="{ columnId, uniqueValues, minMax }">
<fieldset v-if="columnId === 'category'">
<label v-for="v in uniqueValues" :key="v"><input type="checkbox" /> {{ v }}</label>
</fieldset>
<input v-else type="range" :min="minMax[0]" :max="minMax[1]" />
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, { Column } from '@rozie-ui/data-table-svelte';
// Faceting is HEADLESS + read-only (NO event, NO built-in control). The #filter slot
// hands you `uniqueValues` (keys, cross-filtered) + numeric `minMax`.
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
let columnFilters = $state<{ id: string; value: unknown }[]>([]);
</script>
<DataTable data={rows} bind:columnFilters>
<Column field="name" header="Name" />
<Column field="category" header="Category" filterable />
<Column field="price" header="Price" filterable />
{#snippet filter({ columnId, uniqueValues, minMax })}
{#if columnId === 'category'}
<fieldset>
{#each uniqueValues as v}<label><input type="checkbox" /> {v}</label>{/each}
</fieldset>
{:else}
<input type="range" min={minMax[0]} max={minMax[1]} />
{/if}
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import { DataTable, Column } from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column],
template: `
<!-- Faceting is HEADLESS + read-only (NO event, NO built-in control). The #filter slot
hands you `uniqueValues` (keys, cross-filtered) + numeric `minMax`. -->
<DataTable [data]="rows" [(columnFilters)]="columnFilters">
<Column field="name" header="Name" />
<Column field="category" header="Category" [filterable]="true" />
<Column field="price" header="Price" [filterable]="true" />
<ng-template #filter let-columnId="columnId" let-uniqueValues="uniqueValues" let-minMax="minMax">
@if (columnId === 'category') {
<fieldset>
@for (v of uniqueValues; track v) { <label><input type="checkbox" /> {{ v }}</label> }
</fieldset>
} @else {
<input type="range" [min]="minMax[0]" [max]="minMax[1]" />
}
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
columnFilters: { id: string; value: unknown }[] = [];
}tsx
import { createSignal } from 'solid-js';
import { DataTable, Column } from '@rozie-ui/data-table-solid';
export function Demo() {
// Faceting is HEADLESS + read-only (NO event, NO built-in control). The #filter slot
// hands you `uniqueValues` (keys, cross-filtered) + numeric `minMax`.
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
const [columnFilters, setColumnFilters] = createSignal<{ id: string; value: unknown }[]>([]);
return (
<DataTable
data={rows}
columnFilters={columnFilters()}
onFilterChange={(p) => p.columnFilters && setColumnFilters(p.columnFilters)}
// The #filter scoped slot is a render prop on Solid (the documented edge).
filterSlot={({ columnId, uniqueValues, minMax }) =>
columnId === 'category' ? (
<fieldset>
{uniqueValues.map((v) => <label><input type="checkbox" /> {String(v)}</label>)}
</fieldset>
) : (
<input type="range" min={minMax?.[0]} max={minMax?.[1]} />
)
}
>
<Column field="name" header="Name" />
<Column field="category" header="Category" filterable />
<Column field="price" header="Price" filterable />
</DataTable>
);
}ts
import { html, render } from 'lit';
import '@rozie-ui/data-table-lit';
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
// Faceting is HEADLESS + read-only (NO event, NO built-in control). The #filter slot is the
// `.filter` property; it receives `uniqueValues` (keys, cross-filtered) + numeric `minMax`.
render(html`
<rozie-data-table
.data=${rows}
.filter=${({ columnId, uniqueValues, minMax }) =>
columnId === 'category'
? html`<fieldset>${uniqueValues.map((v) => html`<label><input type="checkbox" /> ${v}</label>`)}</fieldset>`
: html`<input type="range" min=${minMax?.[0]} max=${minMax?.[1]} />`}
>
<rozie-column field="name" header="Name"></rozie-column>
<rozie-column field="category" header="Category" filterable></rozie-column>
<rozie-column field="price" header="Price" filterable></rozie-column>
</rozie-data-table>
`, document.body);Drop-in editor components (#editor)
tsx
import { useState } from 'react';
import {
DataTable, Column,
EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate,
} from '@rozie-ui/data-table-react';
export function Demo() {
// OPT-IN drop-in editors fill the #editor slot — DataTable stays the headless
// DEFAULT; the editors are additive named exports. Mark each column editor="custom"
// (the drop-in owns rendering) and dispatch by columnId. Each editor takes the slot
// scope as props ({ columnId, column, row, value, commit, cancel }); EditorSelect
// also takes `options`. Use them as-is, or fork one as a template.
const [rows, setRows] = useState([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
return (
<DataTable
interactionMode="grid"
data={rows}
onDataChange={setRows}
onCellEditCommit={(p) => console.log('cell commit', p)}
// The #editor scoped slot is a render prop on React (the documented edge).
renderEditor={(scope) => {
switch (scope.columnId) {
case 'name': return <EditorText {...scope} />;
case 'qty': return <EditorNumber {...scope} />;
case 'status': return <EditorSelect {...scope} options={statusOptions} />;
case 'active': return <EditorCheckbox {...scope} />;
case 'score': return <EditorDate {...scope} />;
default: return null;
}
}}
>
<Column field="name" header="Name" editable editor="custom" />
<Column field="qty" header="Qty" editable editor="custom" />
<Column field="status" header="Status" editable editor="custom" />
<Column field="active" header="Active" editable editor="custom" />
<Column field="score" header="Score" editable editor="custom" />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, {
Column, EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate,
} from '@rozie-ui/data-table-vue';
// OPT-IN drop-in editors fill the #editor slot — DataTable stays the headless DEFAULT
// export; the editors are additive named exports. v-bind the whole slot scope through
// to each drop-in ({ columnId, column, row, value, commit, cancel }); EditorSelect also
// takes :options. Use them as-is, or fork one as a template.
const rows = ref([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
</script>
<template>
<DataTable
interaction-mode="grid"
v-model:data="rows"
@cell-edit-commit="(p) => console.log('cell commit', p)"
>
<Column field="name" header="Name" :editable="true" editor="custom" />
<Column field="qty" header="Qty" :editable="true" editor="custom" />
<Column field="status" header="Status" :editable="true" editor="custom" />
<Column field="active" header="Active" :editable="true" editor="custom" />
<Column field="score" header="Score" :editable="true" editor="custom" />
<!-- One #editor slot, dispatched by columnId, wiring the drop-in editors. -->
<template #editor="scope">
<EditorText v-if="scope.columnId === 'name'" v-bind="scope" />
<EditorNumber v-else-if="scope.columnId === 'qty'" v-bind="scope" />
<EditorSelect v-else-if="scope.columnId === 'status'" v-bind="scope" :options="statusOptions" />
<EditorCheckbox v-else-if="scope.columnId === 'active'" v-bind="scope" />
<EditorDate v-else-if="scope.columnId === 'score'" v-bind="scope" />
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, {
Column, EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate,
} from '@rozie-ui/data-table-svelte';
// OPT-IN drop-in editors fill the #editor snippet — DataTable stays the headless
// DEFAULT export; the editors are additive named exports. Spread the snippet scope
// through to each drop-in ({ columnId, column, row, value, commit, cancel });
// EditorSelect also takes `options`. Use them as-is, or fork one as a template.
let rows = $state([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
</script>
<DataTable
interactionMode="grid"
bind:data={rows}
oncelleditcommit={(p) => console.log('cell commit', p)}
>
<Column field="name" header="Name" editable editor="custom" />
<Column field="qty" header="Qty" editable editor="custom" />
<Column field="status" header="Status" editable editor="custom" />
<Column field="active" header="Active" editable editor="custom" />
<Column field="score" header="Score" editable editor="custom" />
<!-- One #editor snippet, dispatched by columnId, wiring the drop-in editors. -->
{#snippet editor(scope)}
{#if scope.columnId === 'name'}
<EditorText {...scope} />
{:else if scope.columnId === 'qty'}
<EditorNumber {...scope} />
{:else if scope.columnId === 'status'}
<EditorSelect {...scope} options={statusOptions} />
{:else if scope.columnId === 'active'}
<EditorCheckbox {...scope} />
{:else if scope.columnId === 'score'}
<EditorDate {...scope} />
{/if}
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import {
DataTable, Column,
EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate,
} from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column, EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate],
template: `
<!-- OPT-IN drop-in editors fill the #editor template — DataTable stays the headless
default; the editors are additive named exports. Each takes the slot scope as
inputs ([columnId] [column] [row] [value] [commit] [cancel]); EditorSelect also
takes [options]. Mark each column editor="custom" so the #editor slot drives it. -->
<DataTable
interactionMode="grid"
[(data)]="rows"
(cell-edit-commit)="onCellCommit($event)"
>
<Column field="name" header="Name" [editable]="true" editor="custom" />
<Column field="qty" header="Qty" [editable]="true" editor="custom" />
<Column field="status" header="Status" [editable]="true" editor="custom" />
<Column field="active" header="Active" [editable]="true" editor="custom" />
<Column field="score" header="Score" [editable]="true" editor="custom" />
<ng-template #editor let-columnId="columnId" let-column="column" let-row="row" let-value="value" let-commit="commit" let-cancel="cancel">
@switch (columnId) {
@case ('name') {
<rozie-editor-text [columnId]="columnId" [column]="column" [row]="row" [value]="value" [commit]="commit" [cancel]="cancel" />
}
@case ('qty') {
<rozie-editor-number [columnId]="columnId" [column]="column" [row]="row" [value]="value" [commit]="commit" [cancel]="cancel" />
}
@case ('status') {
<rozie-editor-select [columnId]="columnId" [column]="column" [row]="row" [value]="value" [commit]="commit" [cancel]="cancel" [options]="statusOptions" />
}
@case ('active') {
<rozie-editor-checkbox [columnId]="columnId" [column]="column" [row]="row" [value]="value" [commit]="commit" [cancel]="cancel" />
}
@case ('score') {
<rozie-editor-date [columnId]="columnId" [column]="column" [row]="row" [value]="value" [commit]="commit" [cancel]="cancel" />
}
}
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
];
statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
onCellCommit(p: unknown) { console.log('cell commit', p); }
}tsx
import { createSignal, Switch, Match } from 'solid-js';
import {
DataTable, Column,
EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate,
} from '@rozie-ui/data-table-solid';
export function Demo() {
// OPT-IN drop-in editors fill the #editor slot — DataTable stays the headless
// default; the editors are additive named exports. Spread the slot scope through to
// each drop-in ({ columnId, column, row, value, commit, cancel }); EditorSelect also
// takes `options`. Use them as-is, or fork one as a template.
const [rows, setRows] = createSignal([
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
]);
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
return (
<DataTable
interactionMode="grid"
data={rows()}
onDataChange={setRows}
onCellEditCommit={(p) => console.log('cell commit', p)}
// The #editor scoped slot is a render prop on Solid (the documented edge).
editorSlot={(scope) => (
<Switch>
<Match when={scope.columnId === 'name'}><EditorText {...scope} /></Match>
<Match when={scope.columnId === 'qty'}><EditorNumber {...scope} /></Match>
<Match when={scope.columnId === 'status'}><EditorSelect {...scope} options={statusOptions} /></Match>
<Match when={scope.columnId === 'active'}><EditorCheckbox {...scope} /></Match>
<Match when={scope.columnId === 'score'}><EditorDate {...scope} /></Match>
</Switch>
)}
>
<Column field="name" header="Name" editable editor="custom" />
<Column field="qty" header="Qty" editable editor="custom" />
<Column field="status" header="Status" editable editor="custom" />
<Column field="active" header="Active" editable editor="custom" />
<Column field="score" header="Score" editable editor="custom" />
</DataTable>
);
}ts
import { html, render } from 'lit';
// The single side-effect import registers <rozie-data-table> AND the drop-in editor
// custom elements (<rozie-editor-text>, <rozie-editor-number>, <rozie-editor-select>,
// <rozie-editor-checkbox>, <rozie-editor-date>). DataTable stays the headless default;
// the editors are additive.
import '@rozie-ui/data-table-lit';
let rows = [
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
];
const statusOptions = [
{ value: 'active', label: 'Active' },
{ value: 'archived', label: 'Archived' },
{ value: 'pending', label: 'Pending' },
];
// The #editor slot is the `.editor` property — a function returning a Lit template,
// dispatched by columnId. Pass the slot scope ({ columnId, column, row, value, commit,
// cancel }) as element properties; <rozie-editor-select> also takes `.options`.
render(html`
<rozie-data-table
interaction-mode="grid"
.data=${rows}
@data-change=${(e: CustomEvent) => { rows = e.detail; }}
@cell-edit-commit=${(e: CustomEvent) => console.log('cell commit', e.detail)}
.editor=${({ columnId, column, row, value, commit, cancel }) => {
const p = { columnId, column, row, value, commit, cancel };
switch (columnId) {
case 'name': return html`<rozie-editor-text .columnId=${p.columnId} .column=${p.column} .row=${p.row} .value=${p.value} .commit=${p.commit} .cancel=${p.cancel}></rozie-editor-text>`;
case 'qty': return html`<rozie-editor-number .columnId=${p.columnId} .column=${p.column} .row=${p.row} .value=${p.value} .commit=${p.commit} .cancel=${p.cancel}></rozie-editor-number>`;
case 'status': return html`<rozie-editor-select .columnId=${p.columnId} .column=${p.column} .row=${p.row} .value=${p.value} .commit=${p.commit} .cancel=${p.cancel} .options=${statusOptions}></rozie-editor-select>`;
case 'active': return html`<rozie-editor-checkbox .columnId=${p.columnId} .column=${p.column} .row=${p.row} .value=${p.value} .commit=${p.commit} .cancel=${p.cancel}></rozie-editor-checkbox>`;
case 'score': return html`<rozie-editor-date .columnId=${p.columnId} .column=${p.column} .row=${p.row} .value=${p.value} .commit=${p.commit} .cancel=${p.cancel}></rozie-editor-date>`;
default: return null;
}
}}
>
<rozie-column field="name" header="Name" editable editor="custom"></rozie-column>
<rozie-column field="qty" header="Qty" editable editor="custom"></rozie-column>
<rozie-column field="status" header="Status" editable editor="custom"></rozie-column>
<rozie-column field="active" header="Active" editable editor="custom"></rozie-column>
<rozie-column field="score" header="Score" editable editor="custom"></rozie-column>
</rozie-data-table>
`, document.body);Drop-in filter components (#filter)
tsx
import { useState } from 'react';
import {
DataTable, Column,
FilterText, FilterNumberRange, FilterSelect,
} from '@rozie-ui/data-table-react';
export function Demo() {
// OPT-IN drop-in filters fill the #filter slot — DataTable stays the headless
// DEFAULT; the filters are additive named exports. Mark each column `filterable`
// (the #filter slot only renders for filterable columns) and dispatch by columnId.
// Each filter takes the slot scope as props ({ columnId, column, value, setFilter });
// FilterSelect also reads `uniqueValues`, FilterNumberRange also reads `minMax` —
// both arrive in the slot scope, so spreading it through wires them up.
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
const [columnFilters, setColumnFilters] = useState<{ id: string; value: unknown }[]>([]);
return (
<DataTable
data={rows}
columnFilters={columnFilters}
onFilterChange={(p) => p.columnFilters && setColumnFilters(p.columnFilters)}
// The #filter scoped slot is a render prop on React (the documented edge).
renderFilter={(scope) => {
switch (scope.columnId) {
case 'name': return <FilterText {...scope} />;
case 'category': return <FilterSelect {...scope} />;
case 'price': return <FilterNumberRange {...scope} />;
default: return null;
}
}}
>
<Column field="name" header="Name" filterable />
<Column field="category" header="Category" filterable />
<Column field="price" header="Price" filterable />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, {
Column, FilterText, FilterNumberRange, FilterSelect,
} from '@rozie-ui/data-table-vue';
// OPT-IN drop-in filters fill the #filter slot — DataTable stays the headless DEFAULT
// export; the filters are additive named exports. Mark each column :filterable (the
// #filter slot only renders for filterable columns) and v-bind the whole slot scope
// through to each drop-in ({ columnId, column, value, setFilter, uniqueValues, minMax }).
const rows = ref([
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
]);
const columnFilters = ref<{ id: string; value: unknown }[]>([]);
</script>
<template>
<DataTable v-model:data="rows" v-model:column-filters="columnFilters">
<Column field="name" header="Name" :filterable="true" />
<Column field="category" header="Category" :filterable="true" />
<Column field="price" header="Price" :filterable="true" />
<!-- One #filter slot, dispatched by columnId, wiring the drop-in filters. -->
<template #filter="scope">
<FilterText v-if="scope.columnId === 'name'" v-bind="scope" />
<FilterSelect v-else-if="scope.columnId === 'category'" v-bind="scope" />
<FilterNumberRange v-else-if="scope.columnId === 'price'" v-bind="scope" />
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, {
Column, FilterText, FilterNumberRange, FilterSelect,
} from '@rozie-ui/data-table-svelte';
// OPT-IN drop-in filters fill the #filter snippet — DataTable stays the headless
// DEFAULT export; the filters are additive named exports. Mark each column filterable
// (the #filter snippet only renders for filterable columns) and spread the snippet
// scope ({ columnId, column, value, setFilter, uniqueValues, minMax }) through.
let rows = $state([
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
]);
let columnFilters = $state<{ id: string; value: unknown }[]>([]);
</script>
<DataTable bind:data={rows} bind:columnFilters>
<Column field="name" header="Name" filterable />
<Column field="category" header="Category" filterable />
<Column field="price" header="Price" filterable />
<!-- One #filter snippet, dispatched by columnId, wiring the drop-in filters. -->
{#snippet filter(scope)}
{#if scope.columnId === 'name'}
<FilterText {...scope} />
{:else if scope.columnId === 'category'}
<FilterSelect {...scope} />
{:else if scope.columnId === 'price'}
<FilterNumberRange {...scope} />
{/if}
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import {
DataTable, Column,
FilterText, FilterNumberRange, FilterSelect,
} from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column, FilterText, FilterNumberRange, FilterSelect],
template: `
<!-- OPT-IN drop-in filters fill the #filter template — DataTable stays the headless
default; the filters are additive named exports. Each takes the slot scope as
inputs ([columnId] [column] [value] [setFilter]); FilterSelect also takes
[uniqueValues], FilterNumberRange also takes [minMax]. Mark each column
filterable so the #filter slot renders. -->
<DataTable [(data)]="rows" [(columnFilters)]="columnFilters">
<Column field="name" header="Name" [filterable]="true" />
<Column field="category" header="Category" [filterable]="true" />
<Column field="price" header="Price" [filterable]="true" />
<ng-template #filter let-columnId="columnId" let-column="column" let-value="value" let-setFilter="setFilter" let-uniqueValues="uniqueValues" let-minMax="minMax">
@switch (columnId) {
@case ('name') {
<rozie-filter-text [columnId]="columnId" [column]="column" [value]="value" [setFilter]="setFilter" />
}
@case ('category') {
<rozie-filter-select [columnId]="columnId" [column]="column" [value]="value" [setFilter]="setFilter" [uniqueValues]="uniqueValues" />
}
@case ('price') {
<rozie-filter-number-range [columnId]="columnId" [column]="column" [value]="value" [setFilter]="setFilter" [minMax]="minMax" />
}
}
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
columnFilters: { id: string; value: unknown }[] = [];
}tsx
import { createSignal, Switch, Match } from 'solid-js';
import {
DataTable, Column,
FilterText, FilterNumberRange, FilterSelect,
} from '@rozie-ui/data-table-solid';
export function Demo() {
// OPT-IN drop-in filters fill the #filter slot — DataTable stays the headless
// default; the filters are additive named exports. Mark each column filterable (the
// #filter slot only renders for filterable columns) and spread the slot scope through
// to each drop-in ({ columnId, column, value, setFilter, uniqueValues, minMax }).
const rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
const [columnFilters, setColumnFilters] = createSignal<{ id: string; value: unknown }[]>([]);
return (
<DataTable
data={rows}
columnFilters={columnFilters()}
onFilterChange={(p) => p.columnFilters && setColumnFilters(p.columnFilters)}
// The #filter scoped slot is a render prop on Solid (the documented edge).
filterSlot={(scope) => (
<Switch>
<Match when={scope.columnId === 'name'}><FilterText {...scope} /></Match>
<Match when={scope.columnId === 'category'}><FilterSelect {...scope} /></Match>
<Match when={scope.columnId === 'price'}><FilterNumberRange {...scope} /></Match>
</Switch>
)}
>
<Column field="name" header="Name" filterable />
<Column field="category" header="Category" filterable />
<Column field="price" header="Price" filterable />
</DataTable>
);
}ts
import { html, render } from 'lit';
// The single side-effect import registers <rozie-data-table> AND the drop-in filter
// custom elements (<rozie-filter-text>, <rozie-filter-number-range>,
// <rozie-filter-select>). DataTable stays the headless default; the filters are additive.
import '@rozie-ui/data-table-lit';
let rows = [
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
{ id: 3, name: 'Gamma', category: 'Hardware', price: 10 },
{ id: 4, name: 'Delta', category: 'Service', price: 50 },
];
let columnFilters: { id: string; value: unknown }[] = [];
// The #filter slot is the `.filter` property — a function returning a Lit template,
// dispatched by columnId. Pass the slot scope ({ columnId, column, value, setFilter,
// uniqueValues, minMax }) as element properties.
render(html`
<rozie-data-table
.data=${rows}
.columnFilters=${columnFilters}
@filter-change=${(e: CustomEvent) => { columnFilters = e.detail.columnFilters; }}
.filter=${({ columnId, column, value, setFilter, uniqueValues, minMax }) => {
const p = { columnId, column, value, setFilter, uniqueValues, minMax };
switch (columnId) {
case 'name': return html`<rozie-filter-text .columnId=${p.columnId} .column=${p.column} .value=${p.value} .setFilter=${p.setFilter}></rozie-filter-text>`;
case 'category': return html`<rozie-filter-select .columnId=${p.columnId} .column=${p.column} .value=${p.value} .setFilter=${p.setFilter} .uniqueValues=${p.uniqueValues}></rozie-filter-select>`;
case 'price': return html`<rozie-filter-number-range .columnId=${p.columnId} .column=${p.column} .value=${p.value} .setFilter=${p.setFilter} .minMax=${p.minMax}></rozie-filter-number-range>`;
default: return null;
}
}}
>
<rozie-column field="name" header="Name" filterable></rozie-column>
<rozie-column field="category" header="Category" filterable></rozie-column>
<rozie-column field="price" header="Price" filterable></rozie-column>
</rozie-data-table>
`, document.body);Drop-in group bar + detail panel (#groupBar / #detail)
tsx
import { useState } from 'react';
import {
DataTable, Column, GroupBar, DetailPanel,
} from '@rozie-ui/data-table-react';
export function Demo() {
// OPT-IN drop-ins: GroupBar fills the #groupBar slot (drag the column chips into the
// zone to group; remove tokens; clear) and DetailPanel fills the #detail slot (the
// open row as a key/value list). Both are additive named exports — DataTable stays the
// headless DEFAULT. Forward the slot scope to each; fork either as a starter.
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
const [grouping, setGrouping] = useState<string[]>([]);
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
return (
<DataTable
data={rows}
groupable
expandable
grouping={grouping}
onGroupChange={setGrouping}
expanded={expanded}
onExpandChange={setExpanded}
// The #groupBar / #detail scoped slots are render props on React (the documented edge).
renderGroupBar={(scope) => <GroupBar {...scope} />}
renderDetail={(scope) => <DetailPanel {...scope} />}
>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
</DataTable>
);
}vue
<script setup lang="ts">
import { ref } from 'vue';
import DataTable, {
Column, GroupBar, DetailPanel,
} from '@rozie-ui/data-table-vue';
// OPT-IN drop-ins fill the #groupBar + #detail slots — DataTable stays the headless
// DEFAULT export; both are additive named exports. v-bind the whole slot scope through
// to each (GroupBar gets { grouping, groupableColumns, applyGrouping, clearGrouping };
// DetailPanel gets { row }). Use them as-is, or fork either as a starter.
const rows = ref([
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
]);
const grouping = ref<string[]>([]);
const expanded = ref<Record<string, boolean>>({});
</script>
<template>
<DataTable
:data="rows"
:groupable="true"
:expandable="true"
v-model:grouping="grouping"
v-model:expanded="expanded"
>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<!-- GroupBar fills #groupBar; DetailPanel fills #detail. -->
<template #groupBar="scope">
<GroupBar v-bind="scope" />
</template>
<template #detail="scope">
<DetailPanel v-bind="scope" />
</template>
</DataTable>
</template>svelte
<script lang="ts">
import DataTable, {
Column, GroupBar, DetailPanel,
} from '@rozie-ui/data-table-svelte';
// OPT-IN drop-ins fill the #groupBar + #detail snippets — DataTable stays the headless
// DEFAULT export; both are additive named exports. Spread the snippet scope through to
// each (GroupBar gets { grouping, groupableColumns, applyGrouping, clearGrouping };
// DetailPanel gets { row }). Use them as-is, or fork either as a starter.
let rows = $state([
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
]);
let grouping = $state<string[]>([]);
let expanded = $state<Record<string, boolean>>({});
</script>
<DataTable data={rows} groupable expandable bind:grouping bind:expanded>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<!-- GroupBar fills #groupBar; DetailPanel fills #detail. -->
{#snippet groupBar(scope)}
<GroupBar {...scope} />
{/snippet}
{#snippet detail(scope)}
<DetailPanel {...scope} />
{/snippet}
</DataTable>ts
import { Component } from '@angular/core';
import {
DataTable, Column, GroupBar, DetailPanel,
} from '@rozie-ui/data-table-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DataTable, Column, GroupBar, DetailPanel],
template: `
<!-- OPT-IN drop-ins fill the #groupBar + #detail templates — DataTable stays the
headless default; both are additive named exports. GroupBar takes the group-bar
scope as inputs ([grouping] [groupableColumns] [applyGrouping] [clearGrouping]);
DetailPanel takes [row]. Use them as-is, or fork either as a starter. -->
<DataTable
[data]="rows"
[groupable]="true"
[expandable]="true"
[(grouping)]="grouping"
[(expanded)]="expanded"
>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
<ng-template #groupBar let-grouping="grouping" let-groupableColumns="groupableColumns" let-applyGrouping="applyGrouping" let-clearGrouping="clearGrouping">
<rozie-group-bar [grouping]="grouping" [groupableColumns]="groupableColumns" [applyGrouping]="applyGrouping" [clearGrouping]="clearGrouping" />
</ng-template>
<ng-template #detail let-row="row">
<rozie-detail-panel [row]="row" />
</ng-template>
</DataTable>
`,
})
export class DemoComponent {
rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
grouping: string[] = [];
expanded: Record<string, boolean> = {};
}tsx
import { createSignal } from 'solid-js';
import {
DataTable, Column, GroupBar, DetailPanel,
} from '@rozie-ui/data-table-solid';
export function Demo() {
// OPT-IN drop-ins fill the #groupBar + #detail slots — DataTable stays the headless
// default; both are additive named exports. Spread the slot scope through to each
// (GroupBar gets { grouping, groupableColumns, applyGrouping, clearGrouping };
// DetailPanel gets { row }). Use them as-is, or fork either as a starter.
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
const [grouping, setGrouping] = createSignal<string[]>([]);
const [expanded, setExpanded] = createSignal<Record<string, boolean>>({});
return (
<DataTable
data={rows}
groupable
expandable
grouping={grouping()}
onGroupChange={setGrouping}
expanded={expanded()}
onExpandChange={setExpanded}
// The #groupBar / #detail scoped slots are render props on Solid (the documented edge).
groupBarSlot={(scope) => <GroupBar {...scope} />}
detailSlot={(scope) => <DetailPanel {...scope} />}
>
<Column field="region" header="Region" />
<Column field="category" header="Category" />
<Column field="units" header="Units" aggregationFn="sum" />
</DataTable>
);
}ts
import { html, render } from 'lit';
// The single side-effect import registers <rozie-data-table> AND the drop-in
// <rozie-group-bar> + <rozie-detail-panel> custom elements. DataTable stays the headless
// default; both are additive.
import '@rozie-ui/data-table-lit';
const rows = [
{ id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
{ id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
{ id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
{ id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
];
// #groupBar is the `.groupBar` property; #detail is the `.detail` property — each a
// function returning a Lit template, dispatched at render time. Pass the slot scope
// through as element properties (GroupBar: the group-bar scope; DetailPanel: { row }).
render(html`
<rozie-data-table
.data=${rows}
groupable
expandable
.groupBar=${({ grouping, groupableColumns, applyGrouping, clearGrouping }) =>
html`<rozie-group-bar .grouping=${grouping} .groupableColumns=${groupableColumns} .applyGrouping=${applyGrouping} .clearGrouping=${clearGrouping}></rozie-group-bar>`}
.detail=${({ row }) => html`<rozie-detail-panel .row=${row}></rozie-detail-panel>`}
>
<rozie-column field="region" header="Region"></rozie-column>
<rozie-column field="category" header="Category"></rozie-column>
<rozie-column field="units" header="Units" .aggregationFn=${'sum'}></rozie-column>
</rozie-data-table>
`, document.body);Imperative handle
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:
tsx
import { useRef } from 'react';
import { DataTable, type DataTableHandle } from '@rozie-ui/data-table-react';
const tbl = useRef<DataTableHandle>(null);
// <DataTable ref={tbl} ... />
tbl.current?.toggleAllRows(true);
const selected = tbl.current?.getSelectedRows();
tbl.current?.editRow(0); // full-row edit on row 0
const range = tbl.current?.getSelectedRange(); // the active cell-range rectangle
tbl.current?.expandAll(); // open every expandable row (collapseAll / toggleRowExpanded / getExpandedRows)
tbl.current?.applyGrouping(['region']); // set grouping (clearGrouping to reset)
const cats = tbl.current?.getFacetedUniqueValues('category'); // cross-filtered facet keys (getFacetedMinMaxValues too)vue
<script setup>
import { ref } from 'vue';
const tbl = ref(); // template ref
</script>
<template>
<DataTable ref="tbl" :data="rows" />
<button @click="tbl.clearSelection()">Clear</button>
<button @click="tbl.editRow(0)">Edit row 0</button>
<button @click="console.log(tbl.getSelectedRange())">Read range</button>
<button @click="tbl.expandAll()">Expand all</button>
<button @click="tbl.applyGrouping(['region'])">Group by region</button>
<button @click="console.log(tbl.getFacetedUniqueValues('category'))">Facet keys</button>
</template>svelte
<script>
let tbl; // component instance via bind:this
</script>
<DataTable bind:this={tbl} data={rows} />
<button onclick={() => tbl.clearSelection()}>Clear</button>
<button onclick={() => tbl.editRow(0)}>Edit row 0</button>
<button onclick={() => console.log(tbl.getSelectedRange())}>Read range</button>
<button onclick={() => tbl.expandAll()}>Expand all</button>
<button onclick={() => tbl.applyGrouping(['region'])}>Group by region</button>
<button onclick={() => console.log(tbl.getFacetedUniqueValues('category'))}>Facet keys</button>ts
@Component({ /* ... */ })
export class DemoComponent {
@ViewChild(DataTable) tbl!: DataTable; // or the viewChild() signal
selectAll() { this.tbl.toggleAllRows(true); }
read() { return this.tbl.getSelectedRows(); }
editFirstRow() { this.tbl.editRow(0); } // full-row edit on row 0
readRange() { return this.tbl.getSelectedRange(); } // the active cell-range rectangle
expandEverything() { this.tbl.expandAll(); } // collapseAll / toggleRowExpanded / getExpandedRows
groupByRegion() { this.tbl.applyGrouping(['region']); } // clearGrouping to reset
facetKeys() { return this.tbl.getFacetedUniqueValues('category'); } // getFacetedMinMaxValues too
}tsx
import { DataTable, type DataTableHandle } from '@rozie-ui/data-table-solid';
let handle: DataTableHandle | undefined;
// The ref callback receives the HANDLE object (not the DOM node).
<DataTable ref={(h) => (handle = h)} data={rows} />;
handle?.toggleAllRows(true);
handle?.editRow(0); // full-row edit on row 0
const range = handle?.getSelectedRange(); // the active cell-range rectangle
handle?.expandAll(); // collapseAll / toggleRowExpanded / getExpandedRows
handle?.applyGrouping(['region']); // clearGrouping to reset
const cats = handle?.getFacetedUniqueValues('category'); // getFacetedMinMaxValues toots
// The custom element IS the handle — exposed methods are public element
// methods.
const el = document.querySelector('rozie-data-table');
el.toggleAllRows(true);
const selected = el.getSelectedRows();
el.editRow(0); // full-row edit on row 0
const range = el.getSelectedRange(); // the active cell-range rectangle
el.expandAll(); // collapseAll / toggleRowExpanded / getExpandedRows
el.applyGrouping(['region']); // clearGrouping to reset
const cats = el.getFacetedUniqueValues('category'); // getFacetedMinMaxValues tooSee also
- DataTable — showcase & API — the full prop / event / slot / handle reference, theming, and accessibility.
- DataTable comparison — how it stacks up against the per-framework libraries.
- DataTable — live demo — the real package running in the page, plus the one
.roziesource and all six generated outputs.