# @toolbox-web/grid - AI Implementation Guide > A high-performance, framework-agnostic data grid built with pure TypeScript and native Web Components. Zero runtime dependencies. Works in vanilla JS, React, Angular, Vue, Svelte, and any JavaScript environment. --- ## CRITICAL RULES — Read Before Generating Any Code These rules apply to ALL frameworks. Violating them produces broken code. ### 1. Required Side-Effect Import The grid is a Web Component. You MUST register the custom element: ```typescript import '@toolbox-web/grid'; // ALWAYS required — registers ``` Without this import, `` renders as an unknown HTML element. ### 2. Height and Display Are Required The grid MUST have explicit height and `display: block`. Without it, the grid renders with zero height: ```css tbw-grid { height: 400px; display: block; } /* Or via inline style: style="height: 400px; display: block;" */ ``` ### 3. Editing Is Opt-In via Plugin Using `editable: true` on a column WITHOUT the `EditingPlugin` loaded throws an error: ```typescript // ❌ WRONG — throws "Configuration error: EditingPlugin not loaded" gridConfig: { columns: [{ field: 'name', editable: true }] } // ✅ CORRECT import { EditingPlugin } from '@toolbox-web/grid/plugins/editing'; gridConfig: { columns: [{ field: 'name', editable: true }], plugins: [new EditingPlugin()] } ``` ### 4. Plugin Dependencies Must Load in Order Some plugins depend on others. Load dependencies FIRST: | Plugin | Requires (load first) | |--------|-----------------------| | `ClipboardPlugin` | `SelectionPlugin` | | `UndoRedoPlugin` | `EditingPlugin` | ### 5. Correct Plugin Class Names | Class Name | Import Path | |------------|-------------| | `SelectionPlugin` | `@toolbox-web/grid/plugins/selection` | | `EditingPlugin` | `@toolbox-web/grid/plugins/editing` | | `FilteringPlugin` | `@toolbox-web/grid/plugins/filtering` | | `MultiSortPlugin` | `@toolbox-web/grid/plugins/multi-sort` | | `GroupingRowsPlugin` | `@toolbox-web/grid/plugins/grouping-rows` | | `GroupingColumnsPlugin` | `@toolbox-web/grid/plugins/grouping-columns` | | `TreePlugin` | `@toolbox-web/grid/plugins/tree` | | `MasterDetailPlugin` | `@toolbox-web/grid/plugins/master-detail` | | `PinnedColumnsPlugin` | `@toolbox-web/grid/plugins/pinned-columns` | | `PinnedRowsPlugin` | `@toolbox-web/grid/plugins/pinned-rows` | | `ReorderPlugin` | `@toolbox-web/grid/plugins/reorder` | | `RowReorderPlugin` | `@toolbox-web/grid/plugins/row-reorder` | | `VisibilityPlugin` | `@toolbox-web/grid/plugins/visibility` | | `ColumnVirtualizationPlugin` | `@toolbox-web/grid/plugins/column-virtualization` | | `ResponsivePlugin` | `@toolbox-web/grid/plugins/responsive` | | `ExportPlugin` | `@toolbox-web/grid/plugins/export` | | `PrintPlugin` | `@toolbox-web/grid/plugins/print` | | `ContextMenuPlugin` | `@toolbox-web/grid/plugins/context-menu` | | `ClipboardPlugin` | `@toolbox-web/grid/plugins/clipboard` | | `UndoRedoPlugin` | `@toolbox-web/grid/plugins/undo-redo` | | `ServerSidePlugin` | `@toolbox-web/grid/plugins/server-side` | | `PivotPlugin` | `@toolbox-web/grid/plugins/pivot` | All-in-one import (larger bundle): `import { SelectionPlugin, EditingPlugin, ... } from '@toolbox-web/grid/all';` ### 6. Type Imports ```typescript // For consumer code (referencing the grid element): import type { DataGridElement } from '@toolbox-web/grid'; // ❌ WRONG — GridElement is an internal type for plugin authors import type { GridElement } from '@toolbox-web/grid'; ``` ### 7. Light DOM — No Shadow DOM The grid uses light DOM. CSS cascade works normally. No `::part()` or `::slotted()` needed. ### 8. Em-Based Sizing All grid dimensions use `em` units. To scale the entire grid, change `font-size`: ```css tbw-grid.compact { font-size: 0.875em; } tbw-grid.large { font-size: 1.25em; } ``` --- ## FRAMEWORK DECISION GUIDE Choose the right approach based on your framework: ### React → Use Feature Props (Recommended) ``` import '@toolbox-web/grid-react/features/selection'; // side-effect imports import { DataGrid } from '@toolbox-web/grid-react'; ``` Alternative: Use `gridConfig` prop with `GridConfig` type for React JSX renderers/editors. ### Angular → Use Feature Props (Recommended) ``` import '@toolbox-web/grid-angular/features/selection'; // side-effect imports import { Grid } from '@toolbox-web/grid-angular'; ``` Alternative: Use `[gridConfig]` with `*tbwRenderer` / `*tbwEditor` structural directives for Angular template renderers. ### Vue → Use Feature Props (Recommended) ``` import '@toolbox-web/grid-vue/features/selection'; // side-effect imports import { TbwGrid } from '@toolbox-web/grid-vue'; ``` Alternative: Use `gridConfig` prop, or `TbwGridColumn` with `#cell` / `#editor` slots for Vue template renderers. ### Vanilla JS / Other Frameworks → Use `createGrid()` or direct DOM ``` import '@toolbox-web/grid'; import { createGrid } from '@toolbox-web/grid'; const grid = createGrid({ columns: [...], plugins: [...] }); grid.rows = data; ``` --- ## Feature Props Reference (All Frameworks) Feature props enable declarative plugin loading with tree-shaking. Import the feature once (side-effect), then use the prop. | Feature Prop | Side-Effect Import (React example) | Example Values | |--------------|------------------------------------|----------------| | `selection` | `@toolbox-web/grid-react/features/selection` | `"cell"`, `"row"`, `"range"`, `{ mode: 'range', checkbox: true }` | | `editing` | `features/editing` | `true`, `"click"`, `"dblclick"`, `"manual"` | | `multiSort` | `features/multi-sort` | `true`, `"single"`, `"multi"`, `{ maxSortColumns: 3 }` | | `filtering` | `features/filtering` | `true`, `{ debounceMs: 200 }` | | `clipboard` | `features/clipboard` | `true` (requires selection) | | `undoRedo` | `features/undo-redo` | `true` (requires editing) | | `contextMenu` | `features/context-menu` | `true`, `{ items: [...] }` | | `reorder` | `features/reorder` | `true` | | `rowReorder` | `features/row-reorder` | `true` | | `visibility` | `features/visibility` | `true` | | `pinnedColumns` | `features/pinned-columns` | `true` | | `pinnedRows` | `features/pinned-rows` | `true`, `{ bottom: [...] }` | | `groupingColumns` | `features/grouping-columns` | `true`, `{ columnGroups: [...] }` | | `groupingRows` | `features/grouping-rows` | `{ groupBy: ['department'] }` | | `tree` | `features/tree` | `{ childrenField: 'children' }` | | `columnVirtualization` | `features/column-virtualization` | `true` | | `masterDetail` | `features/master-detail` | `{ renderer: (row) => ... }` | | `responsive` | `features/responsive` | `true`, `{ breakpoint: 768 }` | | `export` | `features/export` | `true`, `{ filename: 'data.csv' }` | | `print` | `features/print` | `true` | | `pivot` | `features/pivot` | `{ rowFields: [...], columnFields: [...] }` | | `serverSide` | `features/server-side` | `{ dataSource: async (params) => ... }` | For Angular: replace `@toolbox-web/grid-react` with `@toolbox-web/grid-angular`. For Vue: replace `@toolbox-web/grid-react` with `@toolbox-web/grid-vue`. Import ALL features at once (for prototyping): `import '@toolbox-web/grid-react/features';` --- ## TASK RECIPES Complete, copy-pasteable examples for common tasks. ### Recipe 1: Basic Read-Only Grid **React:** ```tsx import '@toolbox-web/grid'; import { DataGrid } from '@toolbox-web/grid-react'; function EmployeeGrid({ employees }) { return ( ); } ``` **Angular:** ```typescript import '@toolbox-web/grid'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid], template: ``, }) export class EmployeeGridComponent { employees = [{ id: 1, name: 'Alice', email: 'alice@co.com', department: 'Engineering' }]; columns = [ { field: 'id', header: 'ID', type: 'number', width: 60 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department', sortable: true }, ]; } ``` **Vue:** ```vue ``` **Vanilla:** ```typescript import '@toolbox-web/grid'; import { createGrid } from '@toolbox-web/grid'; const grid = createGrid({ columns: [ { field: 'id', header: 'ID', type: 'number', width: 60 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department', sortable: true }, ], }); grid.rows = employees; grid.style.height = '400px'; document.body.appendChild(grid); ``` ### Recipe 2: Sortable + Filterable + Editable Grid **React (feature props):** ```tsx import '@toolbox-web/grid'; import '@toolbox-web/grid-react/features/selection'; import '@toolbox-web/grid-react/features/editing'; import '@toolbox-web/grid-react/features/filtering'; import '@toolbox-web/grid-react/features/multi-sort'; import { DataGrid } from '@toolbox-web/grid-react'; function EmployeeGrid({ employees }) { const [data, setData] = useState(employees); return ( ); } ``` **Vanilla (plugin instances):** ```typescript import '@toolbox-web/grid'; import { createGrid } from '@toolbox-web/grid'; import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection'; import { EditingPlugin } from '@toolbox-web/grid/plugins/editing'; import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering'; import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort'; const grid = createGrid({ columns: [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true, filterable: true }, { field: 'email', header: 'Email', filterable: true }, { field: 'salary', header: 'Salary', type: 'number', editable: true }, ], plugins: [ new SelectionPlugin({ mode: 'row' }), new EditingPlugin({ trigger: 'dblclick' }), new FilteringPlugin(), new MultiSortPlugin(), ], }); grid.rows = employees; ``` ### Recipe 3: Grid with Custom Renderers & Editors **React:** ```tsx import '@toolbox-web/grid'; import '@toolbox-web/grid-react/features/editing'; import { DataGrid, type GridConfig } from '@toolbox-web/grid-react'; // Use GridConfig (not plain columns) for React JSX renderers const config: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'status', header: 'Status', editable: true, renderer: (ctx) => ( {ctx.value} ), editor: (ctx) => ( ), }, ], }; function EmployeeGrid({ employees }) { const [data, setData] = useState(employees); // Memoize gridConfig to prevent unnecessary re-renders const gridConfig = useMemo(() => config, []); return ( ); } ``` **Angular (structural directives):** ```typescript import '@toolbox-web/grid'; import '@toolbox-web/grid-angular/features/editing'; import { Component } from '@angular/core'; import { Grid, TbwRenderer, TbwEditor } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid, TbwRenderer, TbwEditor], template: ` {{ value }} `, }) export class EmployeeGridComponent { employees = [/* ... */]; columns = [ { field: 'name', header: 'Name' }, { field: 'status', header: 'Status' }, ]; } ``` **Vue (slots):** ```vue ``` **Vanilla:** ```typescript { field: 'status', header: 'Status', editable: true, renderer: (ctx) => { const span = document.createElement('span'); span.className = `badge badge-${ctx.value}`; span.textContent = ctx.value; return span; }, editor: (ctx) => { const select = document.createElement('select'); ['active', 'inactive'].forEach(opt => { const option = document.createElement('option'); option.value = opt; option.textContent = opt; option.selected = opt === ctx.value; select.appendChild(option); }); select.addEventListener('change', () => ctx.commit(select.value)); return select; }, } ``` ### Recipe 4: Tree Data (Hierarchical) ```typescript import { TreePlugin } from '@toolbox-web/grid/plugins/tree'; // Data structure: each row may have a `children` array const data = [ { id: 1, name: 'Engineering', type: 'dept', children: [ { id: 2, name: 'Frontend', type: 'team', children: [ { id: 3, name: 'Alice', type: 'person' }, ]}, ]}, ]; gridConfig: { columns: [{ field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }], plugins: [new TreePlugin({ childrenField: 'children', expandedByDefault: true })], } ``` React feature prop: `` Angular feature prop: `` Vue feature prop: `` ### Recipe 5: Master-Detail (Expandable Rows) ```typescript import { MasterDetailPlugin } from '@toolbox-web/grid/plugins/master-detail'; gridConfig: { columns: [{ field: 'name' }, { field: 'department' }], plugins: [ new MasterDetailPlugin({ renderer: (row, container) => { container.innerHTML = `

${row.name}

Email: ${row.email}

Phone: ${row.phone}

`; }, }), ], } ``` React feature prop: ` { el.innerHTML = '...' } }} />` ### Recipe 6: Server-Side Data (Infinite Scroll) ```typescript import { ServerSidePlugin } from '@toolbox-web/grid/plugins/server-side'; const serverSide = new ServerSidePlugin({ pageSize: 100, maxConcurrentRequests: 2 }); gridConfig: { columns: [...], plugins: [serverSide], } // After grid is ready: grid.ready().then(() => { serverSide.setDataSource({ async getRows(params) { // params: { startRow, endRow, sortModel, filterModel } const response = await fetch('/api/employees', { method: 'POST', body: JSON.stringify({ start: params.startRow, end: params.endRow }), }); const { rows, total } = await response.json(); return { rows, totalRowCount: total }; }, }); }); ``` ### Recipe 7: Row Grouping with Aggregations ```typescript import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows'; gridConfig: { columns: [ { field: 'department', header: 'Department' }, { field: 'name', header: 'Name' }, { field: 'salary', header: 'Salary', type: 'number', aggregator: 'sum' }, ], plugins: [new GroupingRowsPlugin({ groupBy: ['department'], expanded: true })], } ``` React: `` ### Recipe 8: Pivot Table ```typescript import { PivotPlugin } from '@toolbox-web/grid/plugins/pivot'; gridConfig: { columns: [ { field: 'region' }, { field: 'product' }, { field: 'quarter' }, { field: 'revenue', type: 'number' }, ], plugins: [ new PivotPlugin({ rowFields: ['region'], columnFields: ['quarter'], valueFields: [{ field: 'revenue', aggFunc: 'sum', header: 'Total Revenue' }], }), ], } ``` ### Recipe 9: Responsive Mobile Card Layout ```typescript import { ResponsivePlugin } from '@toolbox-web/grid/plugins/responsive'; // Switch to card layout below 600px gridConfig: { plugins: [new ResponsivePlugin({ breakpoint: 600 })], } // Or progressive degradation with multiple breakpoints gridConfig: { plugins: [ new ResponsivePlugin({ breakpoints: [ { maxWidth: 900, hiddenColumns: ['startDate'] }, { maxWidth: 700, hiddenColumns: ['startDate', 'email'] }, { maxWidth: 500, cardLayout: true }, ], }), ], } ``` --- ## Column Configuration Reference ```typescript interface ColumnConfig { field: string; // Required: property key in row data header?: string; // Display label (defaults to field name) type?: 'string' | 'number' | 'date' | 'boolean' | 'select'; // Column data type width?: number | string; // Pixels (number), '1fr', or percentage string minWidth?: number; // Minimum width in pixels maxWidth?: number; // Maximum width in pixels sortable?: boolean; // Enable sorting (default: true) resizable?: boolean; // Enable resize (default: true) editable?: boolean; // Enable editing (requires EditingPlugin) filterable?: boolean; // Enable filtering (requires FilteringPlugin) hidden?: boolean; // Initially hidden lockVisible?: boolean; // Prevent hiding via UI sticky?: 'left' | 'right'; // Pin column (requires PinnedColumnsPlugin) group?: string | { id: string; label?: string }; // Column group (requires GroupingColumnsPlugin) format?: (value: any, row: TRow) => string; // Display formatter renderer?: (ctx: CellRenderContext) => HTMLElement | string; // Custom cell render editor?: (ctx: ColumnEditorContext) => HTMLElement; // Custom cell editor editorParams?: { // Built-in editor configuration options?: { value: string; label: string }[]; // For type: 'select' min?: number; max?: number; step?: number; // For type: 'number' or 'date' maxLength?: number; placeholder?: string; // For type: 'string' }; headerRenderer?: (ctx: HeaderCellContext) => HTMLElement | string; // Full header control headerLabelRenderer?: (ctx: HeaderCellContext) => HTMLElement | string; // Label-only (sort icons auto-added) aggregator?: 'sum' | 'avg' | 'count' | 'min' | 'max' | 'first' | 'last'; // Footer aggregation } ``` ### Built-in Editor Types | Column `type` | Built-in Editor | `editorParams` | |---------------|-----------------|----------------| | `'string'` | Text input | `maxLength`, `placeholder` | | `'number'` | Number input | `min`, `max`, `step` | | `'date'` | Date picker | `min` (string), `max` (string) | | `'boolean'` | Checkbox | (none) | | `'select'` | Dropdown select | `options: [{ value, label }]` | ### Type-Level Defaults Apply renderers, editors, and formatters to ALL columns of a given type: ```typescript gridConfig: { typeDefaults: { currency: { editorParams: { min: 0, step: 0.01 }, format: (value) => `$${value?.toFixed(2) ?? '0.00'}`, }, date: { format: (value) => value ? new Date(value).toLocaleDateString() : '', }, }, columns: [ { field: 'salary', type: 'currency', editable: true }, // Uses currency defaults { field: 'hireDate', type: 'date' }, // Uses date defaults ], } ``` **Resolution order (highest wins):** Column-level → `typeDefaults` → Framework type registry → Built-in defaults. ### Configuration Precedence The grid merges configuration from multiple sources (lowest → highest priority): 1. `gridConfig` property (base configuration) 2. Light DOM elements (``, ``) 3. `columns` property (direct array) 4. Individual props (`fitMode`) --- ## Context Objects **CellRenderContext (for `renderer`):** ```typescript interface CellRenderContext { value: TValue; // Cell value row: TRow; // Full row data rowIndex: number; // Row index column: ColumnConfig; // Column configuration field: string; // Field name cellEl: HTMLElement; // Cell DOM element } ``` **ColumnEditorContext (for `editor`):** ```typescript interface ColumnEditorContext { value: TValue; // Current cell value row: TRow; // Full row data rowIndex: number; // Row index column: ColumnConfig; // Column configuration field: string; // Field name cellEl: HTMLElement; // Cell DOM element commit: (newValue: TValue) => void; // Commit the edit cancel: () => void; // Cancel the edit onValueChange?: (callback: (newValue: TValue) => void) => void; // Register to receive pushed values when the cell is updated externally // (e.g., via updateRow from another cell's commit). Built-in editors // are auto-wired; custom editors should use this for cascade reactivity. } ``` **HeaderCellContext (for `headerRenderer` / `headerLabelRenderer`):** ```typescript interface HeaderCellContext { column: ColumnConfig; // Column configuration value: string; // Header text sortState: 'asc' | 'desc' | null; // Current sort state cellEl: HTMLElement; // Header cell element renderSortIcon: () => HTMLElement | null; // Get sort indicator element renderFilterButton: () => HTMLElement | null; // Get filter button (requires FilteringPlugin) } ``` --- ## Events Listen to grid events via `addEventListener`: ```typescript import { DGEvents } from '@toolbox-web/grid'; grid.addEventListener(DGEvents.CELL_COMMIT, (e) => { console.log('Cell edited:', e.detail); // { field, value, oldValue, row, rowIndex } }); grid.addEventListener(DGEvents.SORT_CHANGE, (e) => { console.log('Sort changed:', e.detail); }); ``` **Key Event Types:** | Event | Detail Properties | |-------|-------------------| | `cell-click` | `row`, `rowIndex`, `colIndex`, `column`, `field`, `value`, `cellEl`, `originalEvent` | | `row-click` | `row`, `rowIndex`, `rowEl`, `originalEvent` | | `cell-change` | `row`, `rowId`, `rowIndex`, `field`, `oldValue`, `newValue`, `changes`, `source` | | `cell-activate` | `row`, `rowIndex`, `colIndex`, `field`, `value`, `cellEl`, `trigger`, `originalEvent` | | `sort-change` | `field`, `direction` (0=none, 1=asc, -1=desc) | | `column-resize` | `field`, `width` | | `column-state-change` | `columns` array with field, order, width, visible, sort | **Editing Events (require EditingPlugin):** | Event | Detail Properties | |-------|-------------------| | `cell-commit` | `row`, `rowIndex`, `field`, `value`, `oldValue` (cancelable) | | `row-commit` | `row`, `rowIndex`, `changes` (cancelable) | | `edit-open` | `row`, `rowIndex` | | `before-edit-close` | `row`, `rowIndex`, `rowId` — fires before state cleared on commit (row mode); lets managed editors flush values | | `edit-close` | `row`, `rowIndex` | | `changed-rows-reset` | — | | `group-toggle` | `key`, `expanded` | | `dirty-change` | `rowId`, `row`, `original`, `type` (requires `dirtyTracking: true`) | **Plugin Events (via `PluginEvents` constant):** | Event | Plugin | Detail Properties | |-------|--------|-------------------| | `selection-change` | Selection | `SelectionChangeDetail` | | `filter-change` | Filtering | `FilterModel` | | `sort-model-change` | MultiSort | `SortModel[]` | | `tree-expand` | Tree | `TreeExpandDetail` | | `detail-expand` | MasterDetail | `DetailExpandDetail` | | `group-expand` | GroupingRows | `GroupExpandDetail` | | `export-start` | Export | `ExportStartDetail` | | `export-complete` | Export | `ExportCompleteDetail` | | `clipboard-copy` | Clipboard | `ClipboardCopyDetail` | | `clipboard-paste` | Clipboard | `ClipboardPasteDetail` | | `context-menu-open` | ContextMenu | `ContextMenuOpenDetail` | | `context-menu-close` | ContextMenu | — | | `history-change` | UndoRedo | `HistoryChangeDetail` | | `server-loading` | ServerSide | `boolean` | | `server-error` | ServerSide | `Error` | | `column-visibility-change` | Visibility | `VisibilityChangeDetail` | | `column-reorder` | Reorder | `ReorderDetail` | **React event handling:** Use the `on` prefix props on ``: ```tsx console.log(e.detail)} onCellChange={(e) => console.log(e.detail)} onSortChange={(e) => console.log(e.detail)} onRowsChange={setData} // Special prop: called when editing commits /> ``` --- ## Plugin Configuration Reference ### SelectionPlugin ```typescript new SelectionPlugin({ mode: 'cell' | 'row' | 'range', // Required multiSelect?: boolean, // Allow multi-select (default: true) checkbox?: boolean, // Show checkbox column checkboxPosition?: 'left' | 'right', selectOnClick?: boolean, // Select on click (default: true in row mode) }) // Plugin queries (for inter-plugin communication via grid.query()): // 'getSelection' → returns current selection state // 'selectRows' → select rows by index array (row mode) // 'getSelectedRowIndices' → returns sorted number[] of selected row indices // 'getSelectedRows' → returns actual row objects (preferred over index lookup) ``` ### EditingPlugin ```typescript new EditingPlugin({ mode?: 'cell' | 'row', // Edit scope (default: 'cell') trigger?: 'click' | 'dblclick' | 'enter' | 'manual', // (default: 'dblclick') commitOnBlur?: boolean, // (default: true) commitOnEnter?: boolean, // (default: true) cancelOnEscape?: boolean, // (default: true) focusTrap?: boolean, // (default: false) — reclaim focus when it leaves during edit dirtyTracking?: boolean, // (default: false) — track row baselines for dirty/pristine state }) // Keyboard: Enter = row edit, F2 = single-cell edit, Escape = cancel, Tab = next cell // Focus Management: grid.registerExternalFocusContainer(el) / unregisterExternalFocusContainer(el) // → overlays appended to (datepickers, dropdowns) are treated as "inside" the grid // → prevents editor close when clicking in registered containers // → Angular BaseOverlayEditor auto-registers its panel; custom editors should register manually // → grid.containsFocus(node?) checks grid DOM + all registered external containers // Dirty Tracking (requires dirtyTracking: true): // isDirty(rowId), isPristine(rowId), dirty, pristine // getDirtyRows() → [{ id, original, current }] // markAsPristine(rowId), markAllPristine(), markAsDirty(rowId) // revertRow(rowId), getOriginalRow(rowId) // Event: 'dirty-change' → { rowId, row, original, type: 'modified'|'pristine'|'reverted' } // CSS classes: .tbw-row-dirty, .tbw-row-new (auto-applied to rows) // Editing Stability: active edit session survives sort/filter/group pipeline changes ``` ### FilteringPlugin ```typescript new FilteringPlugin({ debounceMs?: number, // Debounce input (default: 300) showFilterRow?: boolean, // Show inline filter row (default: false) caseSensitive?: boolean, // Case-sensitive text matching (default: false) trimInput?: boolean, // Trim whitespace from filter input (default: true) useWorker?: boolean, // Offload filtering to a Web Worker (default: false) filterHandler?: (rows, filters) => rows | Promise, // Custom/server-side filtering valuesHandler?: (field, column) => Promise, // Async unique-value provider for filter panels filterPanelRenderer?: (params: FilterPanelParams) => HTMLElement, // Custom filter panel renderer }) // Column-level: { filterValue: (row) => value } — custom extractor for filtering // Blank filtering: number/date panels include a Blank checkbox; set-filter panels show a "(Blank)" entry // Import BLANK_FILTER_VALUE sentinel from '@toolbox-web/grid/plugins/filtering' for programmatic blank filter models // FilterPanelParams.currentFilter gives the active FilterModel so custom panels can pre-populate // Silent mode: pass { silent: true } to setFilter/setFilterModel/clearAllFilters/clearFieldFilter // to update filter state without triggering re-render — apply all at once with a non-silent final call ``` ### TreePlugin ```typescript new TreePlugin({ childrenField?: string, // Children array field (default: 'children') expandedByDefault?: boolean, // Start expanded (default: false) indentSize?: number, // Indent per level in px (default: 20) loadChildren?: (row) => Promise, // Lazy load children }) ``` ### MasterDetailPlugin ```typescript new MasterDetailPlugin({ renderer: (row, container) => void | (() => void), // Detail renderer (required) expandOnRowClick?: boolean, // Expand on row click (default: false) singleExpand?: boolean, // Only one expanded at a time height?: number | 'auto', // Detail panel height }) ``` ### GroupingRowsPlugin ```typescript new GroupingRowsPlugin({ groupBy: string[], // Fields to group by (required) expandedByDefault?: boolean, // Start expanded (default: true) showGroupFooter?: boolean, // Show group footer with aggregations groupRenderer?: (group) => HTMLElement | string, // Custom group row }) ``` ### ServerSidePlugin ```typescript new ServerSidePlugin({ pageSize?: number, // Rows per API request (default: 100) cacheBlockSize?: number, // Cache block size maxConcurrentRequests?: number, // Parallel request limit (default: 2) }) // Then: plugin.setDataSource({ getRows: async (params) => ({ rows, totalRowCount }) }) ``` ### ResponsivePlugin ```typescript new ResponsivePlugin({ breakpoint?: number, // Single breakpoint (px) breakpoints?: BreakpointConfig[], // Progressive degradation animate?: boolean, // Smooth transitions (default: true) cardRenderer?: (row) => HTMLElement, // Custom card renderer }) ``` ### ClipboardPlugin ```typescript new ClipboardPlugin({ includeHeaders?: boolean, // Include headers in copy (default: false) delimiter?: string, // Column delimiter (default: '\t') newline?: string, // Row delimiter (default: '\n') quoteStrings?: boolean, // Wrap strings with quotes (default: false) processCell?: (value, field, row) => string, // Custom value formatter pasteHandler?: PasteHandler | null, // Custom paste handler (null = disable) }) // Programmatic API: const clipboard = grid.getPluginByName('clipboard'); await clipboard.copy(); // Copy current selection await clipboard.copy({ columns: ['name', 'email'], rowIndices: [0, 3, 7], includeHeaders: true }); await clipboard.copyRows([0, 5], { columns: ['name'] }); // Copy specific rows const text = clipboard.getSelectionAsText({ columns: ['name'] }); // Preview without copying await clipboard.paste(); // Read & parse clipboard ``` ### ExportPlugin ```typescript new ExportPlugin({ fileName?: string, // Base filename (default: 'export') includeHeaders?: boolean, // Include headers (default: true) onlyVisible?: boolean, // Export only visible columns (default: true) onlySelected?: boolean, // Export only selected rows (default: false) }) // Programmatic API: const exporter = grid.getPluginByName('export'); exporter.exportCsv(); // Export all visible data exporter.exportCsv({ columns: ['name', 'email'], rowIndices: [0, 3], fileName: 'contacts' }); exporter.exportExcel({ fileName: 'report' }); exporter.exportJson({ fileName: 'data' }); ``` ### ContextMenuPlugin ```typescript new ContextMenuPlugin({ items?: ContextMenuItem[] | ((params: ContextMenuParams) => ContextMenuItem[]), }) // ContextMenuParams passed to action callbacks: interface ContextMenuParams { row: unknown; // Row data (null for headers) rowIndex: number; // Row index (-1 for headers) column: unknown; // Column configuration columnIndex: number; field: string; value: unknown; isHeader: boolean; event: MouseEvent; selectedRows: number[]; // Currently selected row indices (auto-synced with SelectionPlugin) // Note: these are indices into the grid's processed (sorted/filtered) rows, // not the original input array. Prefer getSelectedRows() for row objects. } // Selection sync: right-click on selected row preserves multi-selection; // right-click on unselected row selects only that row. // Uses plugin query system (no hard dependency on SelectionPlugin). ``` ### PivotPlugin ```typescript new PivotPlugin({ active?: boolean, // Whether pivot is active on load (default: true when fields configured) rowGroupFields?: string[], // Fields to group rows by columnGroupFields?: string[], // Fields to create pivot columns from valueFields?: PivotValueField[], // Value fields with aggregation: { field, aggFunc, header? } // aggFunc: 'sum' | 'avg' | 'count' | 'min' | 'max' | 'first' | 'last' showTotals?: boolean, // Show row totals column showGrandTotal?: boolean, // Show grand total row defaultExpanded?: boolean, // Groups expanded by default (default: true) indentWidth?: number, // Indent per depth level in px (default: 20) showToolPanel?: boolean, // Show pivot config tool panel (default: true) animation?: false | 'slide' | 'fade', // Expand/collapse animation (default: 'slide') }) // Programmatic API: const pivot = grid.getPluginByName('pivot'); pivot.enablePivot(); // Activate pivot mode pivot.disablePivot(); // Deactivate pivot mode pivot.isPivotActive(); // Check if pivot is active pivot.getPivotResult(); // Get current PivotResult | null pivot.setRowGroupFields(fields); // Update row group fields pivot.setColumnGroupFields(fields); // Update column group fields pivot.setValueFields(fields); // Update value fields pivot.refresh(); // Force re-compute pivot pivot.expandAll(); // Expand all groups pivot.collapseAll(); // Collapse all groups ``` ### MultiSortPlugin ```typescript new MultiSortPlugin({ maxSortColumns?: number, // Max columns to sort by (default: 3) showSortIndex?: boolean, // Show sort order badges on headers (default: true) }) // Events: 'sort-change' → { sortModel: SortModel[] } // Programmatic API: const sort = grid.getPluginByName('multiSort'); sort.getSortModel(); // Get current SortModel[] (copy) sort.setSortModel(model); // Set sort model programmatically sort.clearSort(); // Remove all sorting sort.getSortIndex(field); // Get 1-based sort index or undefined sort.getSortDirection(field); // Get 'asc' | 'desc' | undefined ``` ### PinnedColumnsPlugin ```typescript new PinnedColumnsPlugin({ // No constructor options — activated by column config: { field: 'id', pinned: 'left' } }) // Column config augmentation (requires PinnedColumnsPlugin): // pinned?: 'left' | 'right' | 'start' | 'end' // 'start'/'end' are logical (flip in RTL) // Programmatic API: const pinned = grid.getPluginByName('pinnedColumns'); pinned.setPinPosition(field, position); // Pin/unpin: position = 'left'|'right'|'start'|'end' | undefined pinned.refreshStickyOffsets(); // Re-apply sticky offsets (e.g., after column resize) pinned.getLeftPinnedColumns(); // Get columns pinned left pinned.getRightPinnedColumns(); // Get columns pinned right pinned.clearStickyPositions(); // Remove all sticky positioning ``` ### PinnedRowsPlugin ```typescript new PinnedRowsPlugin({ position?: 'top' | 'bottom', // Info bar position (default: 'bottom') showRowCount?: boolean, // Show total row count (default: true) showSelectedCount?: boolean, // Show selected count (default: true) showFilteredCount?: boolean, // Show filtered count (default: true) customPanels?: PinnedRowsPanel[], // Custom info bar panels aggregationRows?: AggregationRowConfig[], // Footer/header aggregation rows fullWidth?: boolean, // Default fullWidth for all aggregation rows (default: false) }) // Programmatic API: const rows = grid.getPluginByName('pinnedRows'); rows.refresh(); // Refresh status bar rows.addPanel(panel); // Add a custom info bar panel rows.removePanel(id); // Remove a custom panel by ID rows.addAggregationRow(config); // Add an aggregation row at runtime rows.removeAggregationRow(id); // Remove an aggregation row by ID ``` ### ReorderPlugin ```typescript new ReorderPlugin({ animation?: false | 'flip' | 'fade', // Animation type (default: 'flip') animationDuration?: number, // Animation duration ms (default: 200) }) // Events: 'column-move' (cancelable) → { field, fromIndex, toIndex, columnOrder } // Programmatic API: const reorder = grid.getPluginByName('reorder'); reorder.getColumnOrder(); // Get current column order (string[]) reorder.moveColumn(field, toIndex); // Move column to new position reorder.setColumnOrder(order); // Set explicit column order reorder.resetColumnOrder(); // Reset to original config order ``` ### RowReorderPlugin ```typescript new RowReorderPlugin({ enableKeyboard?: boolean, // Ctrl+Up/Down shortcuts (default: true) showDragHandle?: boolean, // Show drag handle column (default: true) dragHandlePosition?: 'left' | 'right', // Handle column position (default: 'left') dragHandleWidth?: number, // Handle column width in px (default: 40) canMove?: (row, fromIndex, toIndex, direction) => boolean, // Validation callback debounceMs?: number, // Keyboard move debounce ms (default: 150) animation?: false | 'flip', // Row movement animation (default: 'flip') }) // Events: 'row-move' (cancelable) → { row, fromIndex, toIndex, rows, source } // Programmatic API: const rowReorder = grid.getPluginByName('rowReorder'); rowReorder.moveRow(fromIndex, toIndex); // Move a row programmatically rowReorder.canMoveRow(fromIndex, toIndex); // Check if move is allowed ``` ### VisibilityPlugin ```typescript new VisibilityPlugin({ allowHideAll?: boolean, // Allow hiding all columns (default: false) }) // Programmatic API: const vis = grid.getPluginByName('visibility'); vis.show(); // Open visibility tool panel vis.hide(); // Close tool panel vis.toggle(); // Toggle visibility panel vis.isPanelVisible(); // Check if panel is visible vis.isColumnVisible(field); // Check column visibility vis.setColumnVisible(field, visible); // Set column visibility vis.toggleColumn(field); // Toggle a column's visibility vis.showAll(); // Show all columns vis.getVisibleColumns(); // Get visible field names vis.getHiddenColumns(); // Get hidden field names ``` ### ColumnVirtualizationPlugin ```typescript new ColumnVirtualizationPlugin({ autoEnable?: boolean, // Auto-enable above threshold (default: true) threshold?: number, // Column count threshold (default: 30) overscan?: number, // Extra columns per side (default: 3) }) // Programmatic API: const colVirt = grid.getPluginByName('columnVirtualization'); colVirt.getIsVirtualized(); // Check if virtualization is active colVirt.getVisibleColumnRange(); // Get { start, end } column indices colVirt.scrollToColumn(columnIndex); // Scroll to bring column into view colVirt.getTotalWidth(); // Get total width of all columns (px) ``` ### GroupingColumnsPlugin ```typescript new GroupingColumnsPlugin({ groupHeaderRenderer?: (params) => HTMLElement | string | void, // Custom group header showGroupBorders?: boolean, // Show borders between groups (default: true) lockGroupOrder?: boolean, // Prevent reorder across groups (default: false) }) // Column config augmentation: // group?: { id: string; label?: string } | string // GridConfig augmentation: // columnGroups?: ColumnGroupDefinition[] — { id, header, children: string[] } // Programmatic API: const groups = grid.getPluginByName('groupingColumns'); groups.isGroupingActive(); // Check if groups are active groups.getGroups(); // Get computed ColumnGroup[] groups.getGroupColumns(groupId); // Get columns in a specific group groups.refresh(); // Recompute groups from current columns ``` ### PrintPlugin ```typescript new PrintPlugin({ button?: boolean, // Show print button in toolbar (default: false) orientation?: 'portrait' | 'landscape', // Page orientation (default: 'landscape') warnThreshold?: number, // Confirm dialog above N rows (default: 500, 0 = disabled) maxRows?: number, // Hard limit on rows to print (default: 0 = unlimited) includeTitle?: boolean, // Include grid title in output (default: true) includeTimestamp?: boolean, // Include timestamp in footer (default: true) title?: string, // Custom print title (overrides shell title) isolate?: boolean, // Hide other content during print (default: false) }) // Column config augmentation: // printHidden?: boolean // Hide column when printing (default: false) // Programmatic API: const printer = grid.getPluginByName('print'); printer.isPrinting(); // Check if print is in progress await printer.print(); // Open browser print dialog await printer.print({ orientation, title, maxRows }); // Override config per print ``` ### UndoRedoPlugin ```typescript new UndoRedoPlugin({ maxHistorySize?: number, // Max undo actions in stack (default: 100) }) // Keyboard: Ctrl+Z = undo, Ctrl+Y / Ctrl+Shift+Z = redo // Dependencies: Requires EditingPlugin // Programmatic API: const undoRedo = grid.getPluginByName('undoRedo'); undoRedo.undo(); // Undo last action (single or compound) undoRedo.redo(); // Redo last undone action undoRedo.canUndo(); // Check if undo stack is non-empty undoRedo.canRedo(); // Check if redo stack is non-empty undoRedo.clearHistory(); // Clear all undo/redo history undoRedo.getUndoStack(); // Get copy of undo stack undoRedo.getRedoStack(); // Get copy of redo stack undoRedo.recordEdit(idx, field, old, new); // Manually record a cell edit // Compound actions (transaction API): // Group cascaded edits into a single undo step undoRedo.beginTransaction(); // Start buffering edits undoRedo.recordEdit(idx, 'total', oldTotal, newTotal); grid.updateRow(rowId, { total: newTotal }); queueMicrotask(() => undoRedo.endTransaction()); // Push compound to stack // Undo reverts ALL grouped edits in reverse; redo replays in forward order ``` --- ## Grid API ### Factory Functions ```typescript import { createGrid, queryGrid } from '@toolbox-web/grid'; // Create a typed grid element const grid = createGrid({ columns: [{ field: 'name' }], plugins: [new SelectionPlugin()], }); grid.rows = employees; // ✓ Fully typed // Query existing grid const existing = queryGrid('#my-grid'); ``` ### Grid Properties | Property | Type | Description | |----------|------|-------------| | `rows` | `TRow[]` | Row data | | `columns` | `ColumnConfig[]` | Column definitions (shorthand) | | `gridConfig` | `GridConfig` | Full configuration object | | `fitMode` | `'stretch' \| 'fixed' \| 'auto'` | Column fit behavior | | `loading` | `boolean` | Show loading overlay | | `columnState` | `GridColumnState` | Save/restore column state | | `focusedCell` | `{ rowIndex, colIndex, field } \| null` | Currently focused cell (readonly) | ### Grid Methods | Method | Description | |--------|-------------| | `ready()` | Promise that resolves when grid is fully rendered | | `forceLayout()` | Force recalculate layout | | `resetColumnState()` | Reset column widths, order, visibility | | `getPluginByName(name)` | Get plugin instance by name string — **preferred** (type-safe, no import needed) | | `getPlugin(PluginClass)` | Get plugin instance by class (requires import) | | `animateRow(index, type)` | Animate row ('change', 'insert', 'remove') → `Promise` | | `animateRows(indices, type)` | Animate multiple rows → `Promise` | | `animateRowById(id, type)` | Animate row by ID → `Promise` | | `insertRow(index, row, animate?)` | Insert row at visible position (auto-animates) → `Promise` | | `removeRow(index, animate?)` | Remove row at visible position (auto-animates) → `Promise` | | `focusCell(rowIndex, column)` | Focus cell by row index and column index or field name | | `scrollToRow(rowIndex, options?)` | Scroll row into view (align: start/center/end/nearest, behavior: instant/smooth) | | `scrollToRowById(rowId, options?)` | Scroll to row by ID | | `setRowLoading(id, loading)` | Row-level loading indicator | | `setCellLoading(rowId, field, loading)` | Cell-level loading indicator | | `clearAllLoading()` | Clear all loading states | ### Loading States ```typescript // Grid-level loading grid.loading = true; const data = await fetchData(); grid.rows = data; grid.loading = false; // Row-level loading grid.setRowLoading('row-123', true); await saveRow(row); grid.setRowLoading('row-123', false); // Cell-level loading grid.setCellLoading('row-123', 'email', true); await validateEmail(email); grid.setCellLoading('row-123', 'email', false); // Custom loading renderer gridConfig: { loadingRenderer: (ctx) => { const el = document.createElement('div'); el.innerHTML = ``; return el; // ctx.size: 'large' (grid) or 'small' (row/cell) }, } ``` ### Column State Persistence ```typescript // Save state on changes grid.addEventListener('column-state-change', (e) => { localStorage.setItem('grid-prefs', JSON.stringify(e.detail)); }); // Restore state gridConfig: { columnState: JSON.parse(localStorage.getItem('grid-prefs')), } // Reset grid.resetColumnState(); ``` ### Row Animation All animation methods return **Promises** that resolve when the animation completes. ```typescript await grid.animateRow(5, 'change'); // Flash highlight for updated row await grid.animateRows([0, 1, 2], 'change'); // Animate multiple rows ``` ### Insert & Remove Rows `insertRow` and `removeRow` operate on the current visible view without re-running the sort/filter pipeline. Both auto-animate by default. ```typescript // Insert with animation grid.insertRow(3, newEmployee); // Insert without animation grid.insertRow(3, newEmployee, false); // Remove with animation, await completion await grid.removeRow(5); // Remove without animation const removed = await grid.removeRow(5, false); ``` Source data is updated automatically. Use `grid.rows = freshData` for bulk refreshes. ### Animation Configuration ```typescript gridConfig: { animation: { mode: 'on', easing: 'ease-in-out', duration: 200 }, } ``` ### Shell (Header, Toolbar, Tool Panel) ```typescript gridConfig: { shell: { header: { title: 'Employee Directory', toolbarContents: [{ id: 'export-btn', order: 10, render: (container) => { const btn = document.createElement('button'); btn.className = 'tbw-toolbar-btn'; btn.textContent = 'Export'; btn.onclick = () => exportData(grid.rows); container.appendChild(btn); }, }], }, toolPanel: { position: 'right', width: 280, closeOnClickOutside: true }, }, plugins: [new VisibilityPlugin()], // Adds column visibility panel } ``` **Light DOM Shell:** ```html 20 employees ``` --- ## Theming ### CSS Custom Properties (Key Variables) ```css tbw-grid { /* Colors */ --tbw-color-bg: transparent; --tbw-color-fg: #333; --tbw-color-accent: #1976d2; --tbw-color-border: #e0e0e0; --tbw-color-header-bg: #f5f5f5; --tbw-color-row-hover: #f0f7ff; --tbw-color-selection: #e3f2fd; --tbw-color-row-alt: transparent; /* Dimensions (em-based — scale with font-size) */ --tbw-row-height: 1.75em; /* ~28px at 16px font */ --tbw-header-height: 1.875em; /* ~30px */ --tbw-cell-padding: 0.5em; /* Focus & Selection */ --tbw-focus-outline: 2px solid var(--tbw-color-accent); --tbw-range-selection-bg: rgba(25, 118, 210, 0.1); --tbw-range-border-color: var(--tbw-color-accent); /* Animation */ --tbw-animation-duration: 200ms; --tbw-animation-easing: ease-out; } ``` ### Built-in Themes ```typescript import '@toolbox-web/themes/dg-theme-material.css'; // or: dg-theme-bootstrap.css, dg-theme-vibrant.css, // dg-theme-contrast.css, dg-theme-large.css, dg-theme-standard.css ``` ### Row Styling ```typescript gridConfig: { rowClass: (row) => { const classes = []; if (row.status === 'active') classes.push('active-row'); if (row.priority === 'high') classes.push('priority-high'); return classes; }, } ``` ```css tbw-grid .data-grid-row.active-row { background-color: #e8f5e9; } tbw-grid .data-grid-row.priority-high { color: #d32f2f; } ``` --- ## FRAMEWORK DEEP DIVE: React ### Import Structure ```tsx import '@toolbox-web/grid'; // Register web component (REQUIRED) import { DataGrid, type GridConfig } from '@toolbox-web/grid-react'; // React wrapper import '@toolbox-web/grid-react/features/selection'; // Feature side-effect imports ``` ### `columns` Prop vs `gridConfig` Prop - **`columns` prop**: Accepts simple column configs. Does NOT support React JSX renderers/editors. - **`gridConfig` prop**: Accepts `GridConfig` type with `renderer`/`editor` returning JSX. Must be memoized with `useMemo()` if defined inside a component. ```tsx // Simple columns — use columns prop // JSX renderers — use gridConfig prop const config: GridConfig = { columns: [{ field: 'status', renderer: (ctx) => {ctx.value} }], }; config, [])} /> ``` ### Feature-Scoped Hooks ```tsx import '@toolbox-web/grid-react/features/export'; import { useGridExport } from '@toolbox-web/grid-react/features/export'; function MyGrid() { const { exportToCsv, isExporting } = useGridExport(); return (
); } ``` ### App-Wide Providers ```tsx import { GridProvider, GridTypeProvider, GridIconProvider } from '@toolbox-web/grid-react'; // Combined provider <>${ctx.value} } }} > ``` ### Key React Pitfalls 1. **Always memoize `gridConfig`** when defined inside a component. Without `useMemo()`, React creates a new object every render, causing the grid to re-initialize. 2. **No `icons` prop on ``**. Use `` or `gridConfig={{ icons: {...} }}`. 3. **`onRowsChange`** is the React-specific callback for edit commits. It receives the full updated rows array. --- ## FRAMEWORK DEEP DIVE: Angular ### Import Structure ```typescript import '@toolbox-web/grid'; // Register web component (REQUIRED) import { Grid, TbwRenderer, TbwEditor } from '@toolbox-web/grid-angular'; import '@toolbox-web/grid-angular/features/selection'; // Feature side-effect imports ``` ### No CUSTOM_ELEMENTS_SCHEMA Needed The `Grid` directive's selector is `'tbw-grid'`, so Angular recognizes the element automatically when you import `Grid`. No `schemas: [CUSTOM_ELEMENTS_SCHEMA]` is needed. ### Template Renderers (`*tbwRenderer` / `*tbwEditor`) ```html ``` ### Component-Based Renderers (Zero-Template Pattern) Pass Angular component classes directly in `gridConfig` for reusable renderers: ```typescript import { AngularGridAdapter, type GridConfig } from '@toolbox-web/grid-angular'; @Component({ /* ... */ }) export class GridComponent { private adapter = inject(AngularGridAdapter); private config: GridConfig = { columns: [ { field: 'status', renderer: StatusBadgeComponent, editor: StatusEditorComponent }, ], plugins: [new EditingPlugin()], }; // IMPORTANT: Must process config to convert component classes to functions processedConfig = this.adapter.processGridConfig(this.config); } ``` **Renderer component** implements `CellRenderer` with `value`, `row`, `column` inputs. **Editor component** extends `BaseGridEditor` with `commitValue(v)` and `cancelEdit()` methods. ### Reactive Forms Integration ```typescript import { Grid, GridFormArray, TbwEditor } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid, GridFormArray, TbwEditor, ReactiveFormsModule], template: `
`, }) export class MyComponent { form = inject(FormBuilder).group({ employees: this.fb.array([ this.fb.group({ name: ['Alice', Validators.required], age: [30, Validators.min(18)] }), ]), }); } ``` **Lazy form** for large datasets: Use `[lazyForm]="createRowFormFn"` instead of `[formArray]` — creates FormGroups on-demand when editing. ### Feature-Scoped Injectables ```typescript import '@toolbox-web/grid-angular/features/export'; import { injectGridExport } from '@toolbox-web/grid-angular/features/export'; export class MyComponent { gridExport = injectGridExport(); // gridExport.exportToCsv('data.csv') } ``` ### App-Wide Providers ```typescript import { provideGridIcons, provideGridTypeDefaults } from '@toolbox-web/grid-angular'; bootstrapApplication(AppComponent, { providers: [ provideGridIcons({ expand: '➕', collapse: '➖' }), provideGridTypeDefaults({ currency: { renderer: CurrencyComponent } }), ], }); ``` ### Base Classes for Custom Editors & Filters Angular adapter provides base classes to reduce boilerplate when creating custom editors and filter panels. | Class | Extends | Purpose | |-------|---------|---------| | `BaseGridEditor` | — | Common inputs/outputs, validation helpers, edit-close lifecycle | | `BaseGridEditorCVA` | `BaseGridEditor` | Adds `ControlValueAccessor` so same component works in grid and standalone forms | | `BaseOverlayEditor` | `BaseGridEditor` | Floating overlay panel with CSS Anchor Positioning + JS fallback, focus gating, click-outside | | `BaseFilterPanel` | — | Ready-made `params` input for `FilteringPlugin`, `applyAndClose()` / `clearAndClose()` helpers | **Overlay editor example:** ```typescript import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { BaseOverlayEditor } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-date-editor', template: `
` }) export class DateEditorComponent extends BaseOverlayEditor implements AfterViewInit { @ViewChild('panel') panelRef!: ElementRef; @ViewChild('inlineInput') inputRef!: ElementRef; protected override overlayPosition = 'below' as const; ngAfterViewInit(): void { this.initOverlay(this.panelRef.nativeElement); if (this.isCellFocused()) this.showOverlay(); } protected getInlineInput() { return this.inputRef?.nativeElement ?? null; } protected onOverlayOutsideClick() { this.hideOverlay(); } selectAndClose(date: string): void { this.commitValue(date); this.hideOverlay(); } } ``` **CVA editor example** (dual grid + form control): ```typescript import { Component, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { BaseGridEditorCVA } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-date-picker', providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DatePickerComponent), multi: true }], template: ` ` }) export class DatePickerComponent extends BaseGridEditorCVA {} ``` **Filter panel example:** ```typescript import { Component, ViewChild, ElementRef } from '@angular/core'; import { BaseFilterPanel } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-text-filter', template: ` ` }) export class TextFilterComponent extends BaseFilterPanel { @ViewChild('input') input!: ElementRef; applyFilter(): void { this.params().applyTextFilter('contains', this.input.nativeElement.value); } } ``` Use in column config: `{ field: 'name', filterable: true, filterPanel: TextFilterComponent }`. --- ## FRAMEWORK DEEP DIVE: Vue ### Import Structure ```typescript import '@toolbox-web/grid'; // Register web component (REQUIRED) import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue'; import '@toolbox-web/grid-vue/features/selection'; // Feature side-effect imports ``` ### Component Props | Prop | Type | Description | |------|------|-------------| | `rows` | `TRow[]` | Row data (reactive) | | `columns` | `ColumnConfig[]` | Column definitions | | `gridConfig` | `GridConfig` | Full config object | | `fitMode` | `FitMode` | Column fit mode | ### Slot-Based Renderers ```vue ``` ### Composables ```typescript import { useGrid, useGridEvent } from '@toolbox-web/grid-vue'; const { gridElement, forceLayout, getConfig, ready, getPlugin } = useGrid(); useGridEvent('cell-click', (e) => console.log(e.detail)); ``` ### Key Vue Pitfalls 1. **Wrap `gridConfig` in `markRaw()`** to prevent Vue from deeply tracking internal grid state: ```typescript const config = markRaw({ columns: [...], plugins: [...] }); ``` 2. Available sub-components: `TbwGridColumn`, `TbwGridDetailPanel`, `TbwGridToolPanel`, `TbwGridToolButtons`, `TbwGridResponsiveCard`. ### App-Wide Providers ```vue ``` --- ## Migration from Other Grids ### From AG Grid | AG Grid | @toolbox-web/grid | |---------|-------------------| | `` / `` | `` / `` | | `columnDefs` | `columns` or `gridConfig.columns` | | `rowData` | `rows` | | `defaultColDef` | `typeDefaults` | | `headerName` | `header` | | `pinned: 'left'` | `pinned: 'left'` (+ `PinnedColumnsPlugin`) | | `filter: true` | `filterable: true` (+ `FilteringPlugin`) | | `cellRenderer` | `renderer` | | `onCellValueChanged` | `addEventListener('cell-commit', ...)` | | Modules (e.g., `RowGroupingModule`) | Plugin instances (e.g., `new GroupingRowsPlugin()`) | ### From TanStack Table | TanStack Table | @toolbox-web/grid | |----------------|-------------------| | `useReactTable()` | Not needed — use `` | | `columnHelper.accessor()` | Column config with `field` | | `cell: (info) => ...` | `renderer: (ctx) => ...` | | `getSortedRowModel()` | `sortable: true` on columns | | `getFilteredRowModel()` | `FilteringPlugin` | | `useVirtualizer()` | Built-in row virtualization | ### From ngx-datatable | ngx-datatable | @toolbox-web/grid | |---------------|-------------------| | `` | `` with `Grid` directive | | `[rows]` | `[rows]` (same!) | | `` | `` | | `[name]` | `header` | | `` | `*tbwRenderer="let value"` | --- ## CSS Variable Reference (Complete) ### Base Design Tokens | Variable | Default | Description | |----------|---------|-------------| | `--tbw-font-size` | `1em` | Base font size | | `--tbw-font-size-sm` | `0.9285em` | Small font | | `--tbw-font-size-xs` | `0.7857em` | Extra small font | | `--tbw-base-icon-size` | `1em` | Base icon size | | `--tbw-base-radius` | `0.25em` | Base border radius | ### Spacing Scale | Variable | Default | ~px at 16px | |----------|---------|-------------| | `--tbw-spacing-xs` | `0.25em` | ~4px | | `--tbw-spacing-sm` | `0.375em` | ~6px | | `--tbw-spacing-md` | `0.5em` | ~8px | | `--tbw-spacing-lg` | `0.75em` | ~12px | | `--tbw-spacing-xl` | `1em` | ~16px | ### Colors | Variable | Description | |----------|-------------| | `--tbw-color-bg` | Grid background (default: transparent) | | `--tbw-color-panel-bg` | Panel/popup background | | `--tbw-color-fg` | Foreground/text | | `--tbw-color-fg-muted` | Muted/secondary text | | `--tbw-color-accent` | Primary accent color | | `--tbw-color-accent-fg` | Text on accent background | | `--tbw-color-success` | Success state | | `--tbw-color-error` | Error state | | `--tbw-color-selection` | Selected row background | | `--tbw-color-row-alt` | Alternating row background | | `--tbw-color-row-hover` | Row hover background | | `--tbw-color-header-bg` | Header background | | `--tbw-color-header-fg` | Header text | | `--tbw-color-border` | Default border | | `--tbw-color-border-strong` | Emphasized border | | `--tbw-color-border-cell` | Cell border | | `--tbw-color-border-header` | Header border | | `--tbw-color-shadow` | Shadow for dropdowns/panels | ### Dimensions | Variable | Default | Description | |----------|---------|-------------| | `--tbw-row-height` | `1.75em` | Data row height (~28px) | | `--tbw-header-height` | `1.875em` | Header height (~30px) | | `--tbw-cell-padding` | - | Cell padding | | `--tbw-cell-padding-header` | - | Header cell padding | | `--tbw-icon-size` | - | Standard icon size | | `--tbw-icon-size-sm` | `0.875em` | Small icon | | `--tbw-checkbox-size` | - | Checkbox size | | `--tbw-toggle-size` | `1.25em` | Toggle button size | ### Focus & Selection | Variable | Description | |----------|-------------| | `--tbw-focus-outline` | Focus ring style | | `--tbw-focus-outline-offset` | Focus ring offset | | `--tbw-focus-background` | Focused cell background | | `--tbw-range-border-color` | Range selection border | | `--tbw-range-selection-bg` | Range selection background | ### Resize Handle | Variable | Description | |----------|-------------| | `--tbw-resize-handle-width` | Width of resize grab area | | `--tbw-resize-handle-color` | Handle color (transparent default) | | `--tbw-resize-handle-color-hover` | Handle hover color | | `--tbw-resize-indicator-width` | Resize indicator line (2px) | | `--tbw-resize-indicator-color` | Indicator color | ### Animation | Variable | Default | Description | |----------|---------|-------------| | `--tbw-transition-duration` | `120ms` | Quick transitions | | `--tbw-animation-duration` | `200ms` | Standard animations | | `--tbw-animation-easing` | `ease-out` | Animation curve | | `--tbw-animation-enabled` | `1` | Enable/disable (0 or 1) | | `--tbw-row-change-duration` | `500ms` | Row update highlight | | `--tbw-row-insert-duration` | `300ms` | Row insert animation | | `--tbw-row-remove-duration` | `200ms` | Row remove animation | | `--tbw-row-change-color` | - | Row change highlight color | ### Sorting | Variable | Description | |----------|-------------| | `--tbw-sort-indicator-color` | Default sort icon color | | `--tbw-sort-indicator-active-color` | Active sort icon color | | `--tbw-sort-indicator-display` | `inline-flex` (set `none` to hide) | | `--tbw-sort-indicator-visibility` | Show/hide indicators | ### Shell & Tool Panel | Variable | Default | Description | |----------|---------|-------------| | `--tbw-shell-header-height` | `2.75em` | Shell header height | | `--tbw-shell-header-bg` | - | Shell header background | | `--tbw-tool-panel-width` | `17.5em` | Tool panel width | | `--tbw-tool-panel-bg` | - | Tool panel background | ### Plugin-Specific Variables **FilteringPlugin:** `--tbw-filter-btn-display`, `--tbw-filter-btn-visibility`, `--tbw-filter-accent`, `--tbw-filter-panel-bg`, `--tbw-filter-panel-fg`, `--tbw-filter-panel-border`, `--tbw-filter-panel-radius`, `--tbw-filter-panel-shadow`, `--tbw-filter-input-bg`, `--tbw-filter-input-border`, `--tbw-filter-item-height` (28px), `--tbw-filter-hover`, `--tbw-filter-divider`, `--tbw-indicator-size` (0.375rem) **TreePlugin:** `--tbw-tree-depth`, `--tbw-tree-indent-width`, `--tbw-tree-toggle-size` (1.25em), `--tbw-tree-accent` **PivotPlugin:** `--tbw-pivot-group-bg`, `--tbw-pivot-group-hover`, `--tbw-pivot-leaf-bg`, `--tbw-pivot-grand-total-bg`, `--tbw-pivot-toggle-color`, `--tbw-pivot-border`, `--tbw-pivot-section-bg`, `--tbw-pivot-drop-border`, `--tbw-pivot-drop-bg`, `--tbw-pivot-chip-bg`, `--tbw-pivot-chip-border`, `--tbw-pivot-depth` **ResponsivePlugin:** `--tbw-responsive-duration` (200ms) **VisibilityPlugin:** `--tbw-visibility-hover`, `--tbw-visibility-border`, `--tbw-visibility-btn-bg`, `--tbw-reorder-indicator` **SelectionPlugin:** `--tbw-color-warning-bg` (invalid paste feedback) ### Icon Customization ```typescript gridConfig: { icons: { sortAsc: '...', sortDesc: '...', expand: '▶', collapse: '▼', filter: '...', filterActive: '...', dragHandle: '⋮⋮', toolPanel: '☰', }, } ``` --- ## Key Differences from Other Grids 1. **Web Component First**: Works in any framework without wrappers. React/Angular/Vue adapters are optional enhancements. 2. **Plugin Architecture**: Core bundle is lightweight (~42KB gzip). Add features via plugins for optimal tree-shaking. 3. **Single Source of Truth**: All configuration converges to `effectiveConfig`. No conflicting state. 4. **Light DOM**: Uses light DOM for CSS cascade and accessibility. No Shadow DOM isolation issues. 5. **Em-Based Sizing**: Change `font-size` to scale the entire grid proportionally. 6. **Opt-in Editing**: `EditingPlugin` required for inline editing. Prevents accidental edits and keeps core small. 7. **Row Virtualization**: Built-in for 100k+ rows. No extra configuration needed. --- ## Troubleshooting | Problem | Solution | |---------|----------| | "Configuration error: EditingPlugin not loaded" | Add `EditingPlugin` to plugins array | | Grid not rendering / zero height | Add `height: 400px; display: block;` CSS | | React renderers not working | Use `GridConfig` type from `@toolbox-web/grid-react` and `gridConfig` prop (not `columns` prop) | | Angular templates not rendering | Import `Grid` directive from `@toolbox-web/grid-angular` | | Angular component renderers not working | Call `adapter.processGridConfig(config)` before passing to grid | | TypeScript "Property does not exist on GridElement" | Use `DataGridElement` type instead of `GridElement` | | `` renders as empty unknown element | Add `import '@toolbox-web/grid';` side-effect import | --- ## Resources - [Storybook Documentation](https://toolboxjs.com/): Interactive examples - [GitHub Repository](https://github.com/OysteinAmundsen/toolbox): Source code - [npm Package](https://www.npmjs.com/package/@toolbox-web/grid): Installation - [Changelog](https://github.com/OysteinAmundsen/toolbox/blob/main/libs/grid/CHANGELOG.md): Version history