Skip to content

Common Patterns

Real-world grids rarely use a single feature in isolation. This guide shows tested combinations that solve everyday requirements.

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 rows
grid.on('selection-change', () => {
const sel = grid.getPluginByName('selection');
const rows = sel?.getSelectedRows<Employee>() ?? [];
console.log('Selected:', rows);
});

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 committing
grid.on('cell-commit', (detail, e) => {
const { field, value } = detail;
if (field === 'email' && !value.includes('@')) {
e.preventDefault(); // Reject invalid edit
}
});
// Track dirty state
grid.on('dirty-change', () => {
const editing = grid.getPluginByName('editing');
const dirtyRows = editing?.getDirtyRows() ?? [];
saveButton.disabled = dirtyRows.length === 0;
});

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',
},
};

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 button
exportButton.addEventListener('click', () => {
const exp = grid.getPluginByName('export');
exp?.exportCsv({ fileName: 'employees' }); // .csv extension is added automatically
});

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;
},
},
},
};

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
};

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 frame
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
grid.applyTransactionAsync({
update: [{ id: msg.id, changes: msg.changes }],
});
};
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.

ScenarioMethodAnimations
User action, moderate stream (< 10 msg/s)applyTransaction()Yes (configurable)
High-frequency ticker (100+ msg/s)applyTransactionAsync()Disabled (batched)
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt