Common Patterns
Real-world grids rarely use a single feature in isolation. This guide shows tested combinations that solve everyday requirements.
Data Browsing & Selection
Section titled “Data Browsing & Selection”Goal: Let users sort, filter, and select rows from a large dataset.
import '@toolbox-web/grid';import '@toolbox-web/grid/features/selection';import '@toolbox-web/grid/features/filtering';import '@toolbox-web/grid/features/multi-sort';import { queryGrid } from '@toolbox-web/grid';import type { ColumnConfig } from '@toolbox-web/grid';
const grid = queryGrid<Employee>('#grid');grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true, filterable: true }, { field: 'department', header: 'Department', filterable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, ], features: { selection: { mode: 'row', multiSelect: true }, filtering: { debounceMs: 200 }, multiSort: true, },};
// React to selected rowsgrid.on('selection-change', () => { const sel = grid.getPluginByName('selection'); const rows = sel?.getSelectedRows<Employee>() ?? []; console.log('Selected:', rows);});import '@toolbox-web/grid-react/features/selection';import '@toolbox-web/grid-react/features/filtering';import '@toolbox-web/grid-react/features/multi-sort';import { DataGrid, GridColumn, useGrid } from '@toolbox-web/grid-react';
function EmployeeGrid({ employees }) { const gridRef = useGrid<Employee>();
return ( <DataGrid ref={gridRef} rows={employees} selection={{ mode: 'row', multiSelect: true }} filtering={{ debounceMs: 200 }} multiSort onSelectionChange={() => { const rows = gridRef.current?.getPluginByName('selection')?.getSelectedRows<Employee>() ?? []; console.log('Selected:', rows); }} > <GridColumn field="id" header="ID" type="number" sortable /> <GridColumn field="name" header="Name" sortable filterable /> <GridColumn field="department" header="Department" filterable /> <GridColumn field="salary" header="Salary" type="number" sortable /> </DataGrid> );}<script setup lang="ts">import '@toolbox-web/grid-vue/features/selection';import '@toolbox-web/grid-vue/features/filtering';import '@toolbox-web/grid-vue/features/multi-sort';import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';
const gridRef = useGrid<Employee>();
function onSelectionChange() { const rows = gridRef.value?.getPluginByName('selection')?.getSelectedRows<Employee>() ?? []; console.log('Selected:', rows);}</script>
<template> <TbwGrid ref="gridRef" :rows="employees" selection="row" filtering multi-sort @selection-change="onSelectionChange"> <TbwGridColumn field="id" header="ID" type="number" sortable /> <TbwGridColumn field="name" header="Name" sortable filterable /> <TbwGridColumn field="department" header="Department" filterable /> <TbwGridColumn field="salary" header="Salary" type="number" sortable /> </TbwGrid></template>import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';import { GridMultiSortDirective } from '@toolbox-web/grid-angular/features/multi-sort';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, GridSelectionDirective, GridFilteringDirective, GridMultiSortDirective], template: ` <tbw-grid [rows]="employees" [columns]="columns" selection="row" [filtering]="{ debounceMs: 200 }" multiSort (selection-change)="onSelectionChange()" /> `,})export class EmployeeGridComponent { columns = [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true, filterable: true }, { field: 'department', header: 'Department', filterable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, ];
onSelectionChange() { // Access via ViewChild or queryGrid }}Editable Grid with Undo
Section titled “Editable Grid with Undo”Goal: Inline cell editing with full undo/redo support.
import '@toolbox-web/grid/features/editing';import '@toolbox-web/grid/features/undo-redo';import '@toolbox-web/grid/features/selection';
grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, { field: 'active', header: 'Active', type: 'boolean', editable: true }, ], features: { editing: { editOn: 'dblclick', dirtyTracking: true }, undoRedo: true, selection: 'cell', }, getRowId: (row) => row.id, // Required for dirty tracking};
// Validate before committinggrid.on('cell-commit', (detail, e) => { const { field, value } = detail; if (field === 'email' && !value.includes('@')) { e.preventDefault(); // Reject invalid edit }});
// Track dirty stategrid.on('dirty-change', () => { const editing = grid.getPluginByName('editing'); const dirtyRows = editing?.getDirtyRows() ?? []; saveButton.disabled = dirtyRows.length === 0;});import '@toolbox-web/grid-react/features/editing';import '@toolbox-web/grid-react/features/undo-redo';import '@toolbox-web/grid-react/features/selection';import { DataGrid, GridColumn, useGrid } from '@toolbox-web/grid-react';import type { CellCommitDetail } from '@toolbox-web/grid';
function EditableGrid({ employees }) { const gridRef = useGrid<Employee>();
const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>, event?: CustomEvent) => { if (detail.field === 'email' && !detail.value.includes('@')) { event?.preventDefault(); // Reject invalid edit } }, []);
const handleDirtyChange = useCallback(() => { const editing = gridRef.current?.getPluginByName('editing'); const dirtyRows = editing?.getDirtyRows() ?? []; setSaveDisabled(dirtyRows.length === 0); }, []);
return ( <DataGrid ref={gridRef} rows={employees} editing="dblclick" dirtyTracking undoRedo selection="cell" getRowId={(row) => row.id} onCellCommit={handleCellCommit} onDirtyChange={handleDirtyChange} > <GridColumn field="id" header="ID" type="number" /> <GridColumn field="name" header="Name" editable /> <GridColumn field="email" header="Email" editable /> <GridColumn field="active" header="Active" type="boolean" editable /> </DataGrid> );}<script setup lang="ts">import '@toolbox-web/grid-vue/features/editing';import '@toolbox-web/grid-vue/features/undo-redo';import '@toolbox-web/grid-vue/features/selection';import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';import type { CellCommitDetail } from '@toolbox-web/grid';
const gridRef = useGrid<Employee>();const saveDisabled = ref(true);
function onCellCommit(detail: CellCommitDetail) { if (detail.field === 'email' && !detail.value.includes('@')) { detail.preventDefault(); }}
function onDirtyChange() { const editing = gridRef.value?.getPluginByName('editing'); saveDisabled.value = (editing?.getDirtyRows() ?? []).length === 0;}</script>
<template> <TbwGrid ref="gridRef" :rows="employees" editing="dblclick" dirty-tracking undo-redo selection="cell" :get-row-id="(row) => row.id" @cell-commit="onCellCommit" @dirty-change="onDirtyChange" > <TbwGridColumn field="id" header="ID" type="number" /> <TbwGridColumn field="name" header="Name" editable /> <TbwGridColumn field="email" header="Email" editable /> <TbwGridColumn field="active" header="Active" type="boolean" editable /> </TbwGrid></template>import { GridEditingDirective } from '@toolbox-web/grid-angular/features/editing';import { GridUndoRedoDirective } from '@toolbox-web/grid-angular/features/undo-redo';import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, GridEditingDirective, GridUndoRedoDirective, GridSelectionDirective], template: ` <tbw-grid [rows]="employees" [columns]="columns" editing="dblclick" dirtyTracking undoRedo selection="cell" [getRowId]="getRowId" (cell-commit)="onCellCommit($event)" (dirty-change)="onDirtyChange()" /> `,})export class EditableGridComponent { columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, { field: 'active', header: 'Active', type: 'boolean', editable: true }, ]; getRowId = (row: any) => row.id;
onCellCommit(event: CustomEvent) { const { field, value } = event.detail; if (field === 'email' && !value.includes('@')) { event.preventDefault(); } }
onDirtyChange() { // Access editing plugin for dirty rows }}Grouped Data with Aggregates
Section titled “Grouped Data with Aggregates”Goal: Group rows by a field and show aggregated values (sum, average, count).
import '@toolbox-web/grid/features/grouping-rows';import '@toolbox-web/grid/features/selection';
const columns = [ { field: 'department', header: 'Department' }, { field: 'name', header: 'Name' }, { field: 'salary', header: 'Salary', type: 'number', format: (value) => `$${value.toLocaleString()}`, }, { field: 'id', header: 'Count', type: 'number' },];
grid.gridConfig = { columns, features: { groupingRows: { groupOn: (row) => [row.department], defaultExpanded: true, // Aggregators live on the grouping config, keyed by field name. // Built-ins: 'sum' | 'avg' | 'min' | 'max' | 'count' (or a custom function). aggregators: { salary: 'avg', id: 'count' }, }, selection: 'row', },};import '@toolbox-web/grid-react/features/grouping-rows';import '@toolbox-web/grid-react/features/selection';import { DataGrid, GridColumn } from '@toolbox-web/grid-react';
function DepartmentGrid({ rows }) { return ( <DataGrid rows={rows} groupingRows={{ groupOn: (row) => [row.department], defaultExpanded: true, aggregators: { salary: 'avg', id: 'count' }, }} selection={{ mode: 'row' }} > <GridColumn field="department" header="Department" /> <GridColumn field="name" header="Name" /> <GridColumn field="salary" header="Salary" type="number" format={(value) => `$${value.toLocaleString()}`} /> <GridColumn field="id" header="Count" type="number" /> </DataGrid> );}<script setup lang="ts">import '@toolbox-web/grid-vue/features/grouping-rows';import '@toolbox-web/grid-vue/features/selection';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
const formatSalary = (value) => `$${value.toLocaleString()}`;const groupingRows = { groupOn: (row) => [row.department], defaultExpanded: true, aggregators: { salary: 'avg', id: 'count' },};</script>
<template> <TbwGrid :rows="rows" :grouping-rows="groupingRows" :selection="{ mode: 'row' }" > <TbwGridColumn field="department" header="Department" /> <TbwGridColumn field="name" header="Name" /> <TbwGridColumn field="salary" header="Salary" type="number" :format="formatSalary" /> <TbwGridColumn field="id" header="Count" type="number" /> </TbwGrid></template>import { GridGroupingRowsDirective } from '@toolbox-web/grid-angular/features/grouping-rows';import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, GridGroupingRowsDirective, GridSelectionDirective], template: ` <tbw-grid [rows]="rows" [columns]="columns" [groupingRows]="groupingRows" [selection]="{ mode: 'row' }" /> `,})export class DepartmentGridComponent { groupingRows = { groupOn: (row: any) => [row.department], defaultExpanded: true, aggregators: { salary: 'avg', id: 'count' }, }; columns = [ { field: 'department', header: 'Department' }, { field: 'name', header: 'Name' }, { field: 'salary', header: 'Salary', type: 'number', format: (value: number) => `$${value.toLocaleString()}`, }, { field: 'id', header: 'Count', type: 'number' }, ];}Export Selected Rows
Section titled “Export Selected Rows”Goal: Let users filter data, select a subset, and export it.
import '@toolbox-web/grid/features/export';import '@toolbox-web/grid/features/selection';import '@toolbox-web/grid/features/filtering';
grid.gridConfig = { columns: [ { field: 'name', header: 'Name', filterable: true }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department', filterable: true }, ], features: { selection: { mode: 'row', multiSelect: true }, filtering: true, // Always export only selected rows. Omit `onlySelected` (or pass `rowIndices` // at call time) to export the full filtered/visible dataset instead. export: { onlySelected: true }, },};
// Export buttonexportButton.addEventListener('click', () => { const exp = grid.getPluginByName('export'); exp?.exportCsv({ fileName: 'employees' }); // .csv extension is added automatically});import '@toolbox-web/grid-react/features/export';import '@toolbox-web/grid-react/features/selection';import '@toolbox-web/grid-react/features/filtering';import { DataGrid, GridColumn, useGrid } from '@toolbox-web/grid-react';
function ExportableGrid({ employees }) { const gridRef = useGrid<Employee>();
const handleExport = () => { const exp = gridRef.current?.getPluginByName('export'); exp?.exportCsv({ fileName: 'employees' }); };
return ( <> <button onClick={handleExport}>Export CSV</button> <DataGrid ref={gridRef} rows={employees} selection="row" filtering export> <GridColumn field="name" header="Name" filterable /> <GridColumn field="email" header="Email" /> <GridColumn field="department" header="Department" filterable /> </DataGrid> </> );}<script setup lang="ts">import '@toolbox-web/grid-vue/features/export';import '@toolbox-web/grid-vue/features/selection';import '@toolbox-web/grid-vue/features/filtering';import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';
const gridRef = useGrid<Employee>();
function handleExport() { gridRef.value?.getPluginByName('export')?.exportCsv({ fileName: 'employees' });}</script>
<template> <button @click="handleExport">Export CSV</button> <TbwGrid ref="gridRef" :rows="employees" selection="row" filtering export> <TbwGridColumn field="name" header="Name" filterable /> <TbwGridColumn field="email" header="Email" /> <TbwGridColumn field="department" header="Department" filterable /> </TbwGrid></template>import { GridExportDirective } from '@toolbox-web/grid-angular/features/export';import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';import { Component, ViewChild, ElementRef } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, GridExportDirective, GridSelectionDirective, GridFilteringDirective], template: ` <button (click)="handleExport()">Export CSV</button> <tbw-grid #grid [rows]="employees" [columns]="columns" selection="row" filtering export /> `,})export class ExportableGridComponent { @ViewChild('grid') gridEl!: ElementRef; columns = [ { field: 'name', header: 'Name', filterable: true }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department', filterable: true }, ];
handleExport() { this.gridEl.nativeElement.getPluginByName('export')?.exportCsv({ fileName: 'employees' }); }}Master-Detail
Section titled “Master-Detail”Goal: Expand a row to show related detail records.
import '@toolbox-web/grid/features/master-detail';
grid.gridConfig = { columns: [ { field: 'orderId', header: 'Order', type: 'number' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'number' }, ], features: { masterDetail: { // Vanilla signature: (row, rowIndex) => HTMLElement | string detailRenderer: (row) => { const container = document.createElement('div'); container.style.padding = '1rem'; container.innerHTML = `<strong>Order #${row.orderId}</strong>`;
// Nested grid for order items const detail = document.createElement('tbw-grid') as any; detail.style.height = '200px'; detail.style.display = 'block'; detail.columns = [ { field: 'item', header: 'Item' }, { field: 'qty', header: 'Qty', type: 'number' }, { field: 'price', header: 'Price', type: 'number' }, ]; detail.rows = row.items; // Nested data container.appendChild(detail); return container; }, }, },};import '@toolbox-web/grid-react/features/master-detail';import { DataGrid, GridColumn, GridDetailPanel } from '@toolbox-web/grid-react';import type { DetailPanelContext } from '@toolbox-web/grid-react';
function OrderGrid({ orders }: { orders: Order[] }) { return ( <DataGrid rows={orders}> <GridColumn field="orderId" header="Order" type="number" /> <GridColumn field="customer" header="Customer" /> <GridColumn field="total" header="Total" type="number" />
<GridDetailPanel> {(ctx: DetailPanelContext<Order>) => ( <div style={{ padding: '1rem' }}> <strong>Order #{ctx.row.orderId}</strong> <DataGrid rows={ctx.row.items} style={{ height: 200, display: 'block' }}> <GridColumn field="item" header="Item" /> <GridColumn field="qty" header="Qty" type="number" /> <GridColumn field="price" header="Price" type="number" /> </DataGrid> </div> )} </GridDetailPanel> </DataGrid> );}<script setup lang="ts">import '@toolbox-web/grid-vue/features/master-detail';import { TbwGrid, TbwGridColumn, TbwGridDetailPanel } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="orders"> <TbwGridColumn field="orderId" header="Order" type="number" /> <TbwGridColumn field="customer" header="Customer" /> <TbwGridColumn field="total" header="Total" type="number" />
<TbwGridDetailPanel> <template #default="{ row }"> <div style="padding: 1rem"> <strong>Order #{{ row.orderId }}</strong> <TbwGrid :rows="row.items" style="height: 200px; display: block"> <TbwGridColumn field="item" header="Item" /> <TbwGridColumn field="qty" header="Qty" type="number" /> <TbwGridColumn field="price" header="Price" type="number" /> </TbwGrid> </div> </template> </TbwGridDetailPanel> </TbwGrid></template>import { GridDetailView } from '@toolbox-web/grid-angular/features/master-detail';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, GridDetailView], template: ` <tbw-grid [rows]="orders" [columns]="columns"> <tbw-grid-detail> <ng-template let-row> <div style="padding: 1rem"> <strong>Order #{{ row.orderId }}</strong> <tbw-grid [rows]="row.items" [columns]="itemColumns" style="height: 200px; display: block"> </tbw-grid> </div> </ng-template> </tbw-grid-detail> </tbw-grid> `,})export class OrderGridComponent { columns = [ { field: 'orderId', header: 'Order', type: 'number' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'number' }, ]; itemColumns = [ { field: 'item', header: 'Item' }, { field: 'qty', header: 'Qty', type: 'number' }, { field: 'price', header: 'Price', type: 'number' }, ];}High-Volume Data
Section titled “High-Volume Data”Goal: Handle 10k+ rows with pinned identifier columns and column virtualization.
import '@toolbox-web/grid/features/pinned-columns';import '@toolbox-web/grid/features/column-virtualization';import '@toolbox-web/grid/features/multi-sort';import '@toolbox-web/grid/features/filtering';
grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', pinned: 'left', width: 80 }, { field: 'name', header: 'Name', pinned: 'left', width: 180 }, // ... many more columns ], features: { pinnedColumns: true, columnVirtualization: true, multiSort: true, filtering: { debounceMs: 300 }, }, fitMode: 'fixed', // Use natural column widths instead of stretching to fill};import '@toolbox-web/grid-react/features/pinned-columns';import '@toolbox-web/grid-react/features/column-virtualization';import '@toolbox-web/grid-react/features/multi-sort';import '@toolbox-web/grid-react/features/filtering';import { DataGrid, GridColumn } from '@toolbox-web/grid-react';
function LargeDataGrid({ rows }) { return ( <DataGrid rows={rows} pinnedColumns columnVirtualization multiSort filtering={{ debounceMs: 300 }} fitMode="fixed" > <GridColumn field="id" header="ID" type="number" pinned="left" width={80} /> <GridColumn field="name" header="Name" pinned="left" width={180} /> {/* ... many more columns */} </DataGrid> );}<script setup lang="ts">import '@toolbox-web/grid-vue/features/pinned-columns';import '@toolbox-web/grid-vue/features/column-virtualization';import '@toolbox-web/grid-vue/features/multi-sort';import '@toolbox-web/grid-vue/features/filtering';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="rows" pinned-columns column-virtualization multi-sort :filtering="{ debounceMs: 300 }" fit-mode="fixed" > <TbwGridColumn field="id" header="ID" type="number" pinned="left" :width="80" /> <TbwGridColumn field="name" header="Name" pinned="left" :width="180" /> <!-- ... many more columns --> </TbwGrid></template>import { GridPinnedColumnsDirective } from '@toolbox-web/grid-angular/features/pinned-columns';import { GridColumnVirtualizationDirective } from '@toolbox-web/grid-angular/features/column-virtualization';import { GridMultiSortDirective } from '@toolbox-web/grid-angular/features/multi-sort';import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, GridPinnedColumnsDirective, GridColumnVirtualizationDirective, GridMultiSortDirective, GridFilteringDirective], template: ` <tbw-grid [rows]="rows" [columns]="columns" pinnedColumns columnVirtualization multiSort [filtering]="{ debounceMs: 300 }" fitMode="fixed" /> `,})export class LargeDataGridComponent { columns = [ { field: 'id', header: 'ID', type: 'number', pinned: 'left', width: 80 }, { field: 'name', header: 'Name', pinned: 'left', width: 180 }, // ... many more columns ];}Real-Time / Streaming Data
Section titled “Real-Time / Streaming Data”Goal: Push live updates from a WebSocket, SSE, or polling source into the grid efficiently.
Use applyTransaction() to batch add, update, and remove operations into a single render cycle.
For high-frequency streams (many messages per second), applyTransactionAsync() automatically
merges all calls within one animation frame.
import { createGrid } from '@toolbox-web/grid';import type { RowTransaction } from '@toolbox-web/grid';
const grid = createGrid<Trade>('#my-grid');
// --- Low-to-moderate frequency: applyTransaction ---ws.onmessage = (e) => { const msg = JSON.parse(e.data); grid.applyTransaction({ add: msg.type === 'add' ? [msg.row] : undefined, update: msg.type === 'update' ? [{ id: msg.id, changes: msg.changes }] : undefined, remove: msg.type === 'remove' ? [{ id: msg.id }] : undefined, });};
// --- High frequency: applyTransactionAsync ---// Merges rapid calls within a single animation framews.onmessage = (e) => { const msg = JSON.parse(e.data); grid.applyTransactionAsync({ update: [{ id: msg.id, changes: msg.changes }], });};Transaction shape
Section titled “Transaction shape”interface RowTransaction<T> { add?: T[]; // Appended to the end update?: { id: string; changes: Partial<T> }[]; // In-place mutation by row ID remove?: { id: string }[]; // Removed by row ID}Operations are applied in order: removes → updates → adds. This ensures updates don’t target rows about to be removed, and new rows don’t collide with existing IDs.
Choosing between sync and async
Section titled “Choosing between sync and async”| Scenario | Method | Animations |
|---|---|---|
| User action, moderate stream (< 10 msg/s) | applyTransaction() | Yes (configurable) |
| High-frequency ticker (100+ msg/s) | applyTransactionAsync() | Disabled (batched) |
Event Handling Recipes
Section titled “Event Handling Recipes”Practical patterns for wiring grid events into your application — listening across frameworks, reacting to scroll, and acting after a render flush.
Listening to Events Across Frameworks
Section titled “Listening to Events Across Frameworks”The grid dispatches standard CustomEvents that bubble up the DOM. All events use bubbles: true and composed: true. Cancelable events (cell-commit, row-commit, column-move, row-move) honour preventDefault() to reject the action — see the Cancelable Events table.
import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid<Employee>('#my-grid');
// Type-safe event listeninggrid.on('cell-click', ({ row, field }) => { console.log(row, field);});
// Cancelable events — call preventDefault() to reject the actiongrid.on('cell-commit', (detail, e) => { if (!isValid(detail.value)) { e.preventDefault(); // Rejects the edit }});import { DataGrid } from '@toolbox-web/grid-react';import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid';
function EmployeeGrid() { // Event props deliver the unwrapped detail directly const handleCellClick = useCallback((detail: CellClickDetail<Employee>) => { console.log(detail.row, detail.field); }, []);
const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>, e?: CustomEvent) => { if (!isValid(detail.value)) { e?.preventDefault(); // Rejects the edit } }, []);
return ( <DataGrid rows={employees} onCellClick={handleCellClick} onCellCommit={handleCellCommit} /> );}<script setup lang="ts">import { TbwGrid } from '@toolbox-web/grid-vue';import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid';
function onCellClick(event: CustomEvent<CellClickDetail>) { console.log(event.detail.row, event.detail.field);}
function onCellCommit(event: CustomEvent<CellCommitDetail>) { if (!isValid(event.detail.value)) { event.preventDefault(); // Rejects the edit }}</script>
<template> <TbwGrid :rows="employees" @cell-click="onCellClick" @cell-commit="onCellCommit" /></template>import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid';
@Component({ imports: [Grid], template: ` <tbw-grid [rows]="employees" (cellClick)="onCellClick($event)" (cellCommit)="onCellCommit($event)" /> `,})export class EmployeeGridComponent { onCellClick(detail: CellClickDetail<Employee>) { console.log(detail.row, detail.field); }
onCellCommit(detail: CellCommitDetail<Employee>) { if (!isValid(detail.value)) { // To cancel, use the native event via kebab-case binding: // (cell-commit)="onCellCommit($event)" → event.preventDefault() } }}Scroll-Driven Patterns (tbw-scroll)
Section titled “Scroll-Driven Patterns (tbw-scroll)”tbw-scroll fires (rAF-batched) whenever the grid’s vertical viewport scrolls. The detail payload contains scrollTop, scrollHeight, clientHeight, and a direction: 'vertical' discriminator (reserved for future horizontal opt-in). The detail is a fresh object literal each tick — safe to retain, freeze, copy into framework state, or post to a worker.
Infinite scroll / load more
Section titled “Infinite scroll / load more”grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => { if (scrollTop + clientHeight >= scrollHeight - 200) { loadNextPage(); }});Sticky scroll-progress indicator
Section titled “Sticky scroll-progress indicator”A toolbar or progress bar that lives outside the grid and tracks scroll position:
grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => { const max = scrollHeight - clientHeight; const pct = max > 0 ? scrollTop / max : 0; progressBarEl.style.width = `${pct * 100}%`;});For purely visual scroll-driven CSS effects on a different scroller, also consider the native animation-timeline: scroll() — no JS needed.
Defer heavy cell content (Angular @defer style)
Section titled “Defer heavy cell content (Angular @defer style)”Render expensive content (charts, images, embedded video) only when its row is near the viewport:
grid.on('tbw-scroll', ({ scrollTop, clientHeight }) => { // Lazy-mount components in the visible window hydrateHeavyCellsBetween(scrollTop, scrollTop + clientHeight);});Dismiss overlays on scroll
Section titled “Dismiss overlays on scroll”Tooltips, popovers, context menus rendered outside the grid should typically close when the user scrolls:
grid.on('tbw-scroll', () => closeOpenOverlays());Framework adapter names — disambiguated to avoid colliding with native scroll events:
| Adapter | Binding |
|---|---|
| React | <DataGrid onTbwScroll={...} /> |
| Vue | <TbwGrid @tbw-scroll="..." /> |
| Angular | <tbw-grid (tbwScroll)="..." /> |
Render-Completed Patterns (render)
Section titled “Render-Completed Patterns (render)”The render event fires once at the end of every render cycle (the single RAF flush in the render scheduler), after all plugin afterRender hooks have run and after grid.ready() has resolved. Use this when you need to act on the rendered DOM immediately after a programmatic mutation — without resorting to setTimeout or double-requestAnimationFrame hacks.
The detail payload includes the highest render phase that ran (phase), whether this was the first render (initial), the post-processRows row count (rowCount), and the current virtual window (visibleRange, or null when virtualization is disabled).
Focus the first input after addRow in full-grid edit mode
Section titled “Focus the first input after addRow in full-grid edit mode”The original motivation: with editing: { mode: 'grid' } every row is permanently in edit mode. After inserting a new row you want the first cell’s input focused — but the row doesn’t exist in the DOM until the next render.
function addEmployee() { grid.addRow({ id: crypto.randomUUID(), name: '', email: '' }); grid.addEventListener( 'render', () => { const input = grid.querySelector<HTMLInputElement>( '[data-row="0"][data-col="0"] input', ); input?.focus(); }, { once: true }, );}Skip cheap scroll renders
Section titled “Skip cheap scroll renders”The event fires on virtualization-only re-renders too. If you only care about row/column model changes, gate on phase:
import { RenderPhase } from '@toolbox-web/grid';
grid.on('render', ({ phase, rowCount }) => { if (phase < RenderPhase.ROWS) return; // ignore scroll/style-only flushes statusBar.textContent = `${rowCount} rows rendered`;});Framework adapter names:
| Adapter | Binding |
|---|---|
| React | <DataGrid onRender={...} /> |
| Vue | <TbwGrid @render="..." /> |
| Angular | <tbw-grid (render)="..." /> |