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