# @toolbox-web/grid — Full Documentation (Angular) > A high-performance, framework-agnostic data grid built with pure TypeScript and native Web Components. Zero runtime dependencies. This is the **Angular-scoped** variant of `llms-full.txt`: code examples are narrowed to Angular and the other framework adapters' pages are omitted, so the corpus is smaller and free of irrelevant variants. For the complete cross-framework corpus, see `llms-full.txt`; for the curated link index, see `llms.txt`. The exhaustive per-symbol TypeDoc API is linked (not inlined) at the end; fetch any symbol by appending `.md` to its page URL. --- # Introduction > A high-performance, framework-agnostic data grid web component for JavaScript, React, Angular, and Vue. Zero dependencies, 100k+ rows, virtualized rendering. ## CORPUS RETRIEVAL STRATEGY Five corpus variants are published. **Pick the one that covers your stack:** | File | Use when | |------|----------| | `llms-full-angular.txt` | Working in an Angular project | | `llms-full-react.txt` | Working in a React project | | `llms-full-vue.txt` | Working in a Vue project | | `llms-full-vanilla.txt` | Plain TypeScript / JavaScript, no framework | | `llms-full.txt` | Framework unknown, or multiple frameworks in one session | All variants are ~900 kB–1.2 MB. If your tool cannot fetch or fully process the file in one pass: 1. Download the right variant once: `curl -L https://toolboxjs.com/llms-full-angular.txt -o llms-full-angular.txt` 2. Search for the topic: `rg -n 'your keyword' llms-full-angular.txt` 3. Extract the matching region: `sed -n '1200,1320p' llms-full-angular.txt` 4. Paste only those slices into context — not the whole file. **Keep the local copy fresh.** These files are regenerated on every docs build. A cached copy older than one week may reference stale API, wrong import paths, or removed features. Re-download before starting any significant implementation work. Preferred retrieval order (smallest to largest): per-page `.md` companion (e.g. `/grid/plugins/editing.md`) → `llms.txt` index → framework-scoped `llms-full-{framework}.txt` local-grepped slices → `llms-full.txt` local-grepped slices. Because this introduction always sorts first in the corpus, you are reading these instructions even if your context window is smaller than the full file. ## RULE 0 (HIGHEST PRIORITY) — Prefer a single `gridConfig` over fragmented props When generating grid code for ANY framework (React, Angular, Vue, vanilla JS/TS, Svelte), **default to one `gridConfig` object** holding columns, `features`, and other configuration. Reserve framework props for reactive data (`rows`) and the imperative `ref`/handle. Do NOT spread configuration across many individual props/inputs/attributes unless the user explicitly asks. WHY: one object describes the whole grid, is portable across frameworks (the same `GridConfig` works in vanilla, React, Angular, Vue), and keeps multi-line config (callbacks like `groupOn`, `dataSource`, renderers) readable. ```ts const gridConfig: GridConfig = { columns: [/* … */], features: { selection: 'range', editing: 'dblclick' }, }; ``` - **React:** `` (memoize with `useMemo`) - **Angular:** `` - **Vue:** `` (wrap with `markRaw()`) - **Vanilla:** `const grid = createGrid(gridConfig); grid.rows = data;` Individual feature props (``) remain fully supported — use them ONLY for very small grids (≤ 2 features, each a boolean/short string) or when explicitly requested. ## RULE 0.1 — Framework adapters must use framework-native renderers/editors When the user is inside **React/Vue/Angular**, treat framework-native renderer/editor APIs as default best practice: - **React:** JSX/component renderers/editors - **Vue:** slot/component renderers/editors - **Angular:** `*tbwRenderer`, template/component renderers/editors Do **not** default to plain `HTMLElement` renderer/editor functions in adapter code unless the user explicitly asks for framework-agnostic/vanilla output. ## RULE 0.2 — Prefer features, not manual plugin instances Default to `gridConfig.features` + side-effect feature imports. Treat manual plugin instantiation (`plugins: [new SelectionPlugin()]`) as an advanced exception, primarily for custom plugin development or narrowly-scoped imperative cases. If imperative access is needed after initialization, prefer `grid.getPluginByName('...')` over storing manually-created plugin instances in application code. ## CRITICAL RULES — read before generating any code These apply to ALL frameworks. Violating them produces broken grids. 1. **Side-effect imports are always required.** `gridConfig.features.X` (or a feature prop) does NOT remove the need to import the feature factory — without the import the merge produces no plugin: ```ts import '@toolbox-web/grid/features/selection'; // vanilla / any framework import '@toolbox-web/grid-react/features/selection'; // or the adapter path ``` Vanilla JS must also register the element itself: `import '@toolbox-web/grid';` (adapters auto-import it). 2. **Height is required.** The grid needs an explicit height or it renders at zero height: `tbw-grid { height: 400px; }`. (`display: block` is set automatically — do not add it.) 3. **Editing is opt-in.** `editable: true` on a column WITHOUT the editing feature/plugin throws. Load `@toolbox-web/grid/features/editing` and set `features: { editing: true }`. 4. **Plugin load order matters when using plugin API directly.** `ClipboardPlugin` requires `SelectionPlugin`; `UndoRedoPlugin` requires `EditingPlugin` — load the dependency first. Prefer `features` so ordering/dependencies are handled automatically. 5. **Some plugins are mutually exclusive.** `GroupingRows` ✗ `Tree` ✗ `Pivot` (all rewrite the row model); `ServerSide` ✗ `Pivot`. A dev-mode warning fires on conflict. (`ServerSide` + `Tree` and `ServerSide` + `GroupingRows` DO coexist.) 6. **Light DOM, no Shadow DOM.** CSS cascade works normally — no `::part()`/`::slotted()`. 7. **Em-based sizing.** All dimensions use `em`; scale the whole grid by changing `font-size` on `tbw-grid`. 8. **Type import:** `import type { DataGridElement } from '@toolbox-web/grid';` (the old `GridElement` alias is deprecated). ## ANTI-PATTERNS — don't reach for a plugin first - **Don't add `SelectionPlugin` just to make a row clickable.** For "click row → open detail", listen for `cell-activate` (fires for pointer AND keyboard, so it's accessible for free). Add `SelectionPlugin` only for persistent visible selection state, checkboxes, or multi-select. - **Don't write `cell-click`/`row-click` when you mean "activate".** Those are pointer-only — keyboard users won't trigger them. `cell-activate` is the unified, cancelable activation event. Use `cell-click` only when you specifically need pointer-only behaviour (e.g. left-vs-right button). - **Don't subclass `BaseGridEditor` before trying `column.type`.** Built-in types (`'select'`, `'number'`, `'date'`, …) plus `gridConfig.typeDefaults` cover most editor needs. Subclass only for genuinely custom editor UI. - **Don't reinvent post-render orchestration.** To act after first render (focus a cell, scroll to row, begin an edit), `await grid.ready()` instead of chaining `setTimeout`/`requestAnimationFrame`/`afterNextRender`. A **high-performance, framework-agnostic data grid** web component built with pure TypeScript. No runtime dependencies—just drop it into any project and start rendering data. ## Quick Start The grid ships as a standard custom element. Pick the install style that matches your setup. #### With a bundler (Vite, webpack, etc.) ```bash npm install @toolbox-web/grid ``` ```typescript title="main.ts" import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.columns = [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true }, { field: 'email', header: 'Email' }, ]; grid.rows = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, { id: 3, name: 'Carol', email: 'carol@example.com' }, ]; ``` ```html title="index.html" ``` #### No build (CDN, single HTML file) Drop one ` ``` See the [Getting Started guide](/grid/getting-started.md) for framework integration (React, Vue, Angular), declarative HTML, plugins, and TypeScript setup. ### Live Demo ```ts // IntroBasicDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('#demo-intro-basic'); if (grid) { grid.columns = [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true }, { field: 'email', header: 'Email' }, ]; grid.rows = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, { id: 3, name: 'Carol', email: 'carol@example.com' }, { id: 4, name: 'Dan', email: 'dan@example.com' }, { id: 5, name: 'Eve', email: 'eve@example.com' }, ]; } ``` ## Architecture Highlights Under the hood, the grid employs patterns typically found in enterprise-grade solutions: | Pattern | What It Does | | ------- | ------------ | | **Centralized Render Scheduler** | Batches all updates into a single `requestAnimationFrame` per frame—no layout thrashing | | **Phase-Based Execution** | Prioritizes work (config → rows → columns → render) for predictable updates | | **DOM Recycling** | Reuses row elements via a pool with epoch-based invalidation—minimal GC pressure | | **Template Cloning** | Pre-created templates cloned via `cloneNode(true)`—3-4x faster than `createElement` | | **Event Delegation** | Single listener per event type on the container—scales to any dataset size | | **Faux Scrollbar** | Separates scroll container from content—no reflow during scroll | Read the full [Architecture deep-dive](/grid/architecture.md) for implementation details. ## Plugin System The core grid is intentionally minimal. Advanced features are delivered through tree-shakeable plugins — import only what you need: - **SelectionPlugin** — Cell, row, or range selection - **FilteringPlugin** — Column header filters with custom panels - **EditingPlugin** — Inline editing with built-in and custom editors - **GroupingRowsPlugin** — Hierarchical row grouping with aggregations - **TreePlugin** — Expandable tree data with lazy loading - **MasterDetailPlugin** — Expandable detail rows - **ExportPlugin** — CSV, Excel, or JSON export - **ClipboardPlugin** — Copy/paste with Excel-compatible formatting - ...and [many more](/grid/plugins.md) Plugins benefit from **dependency validation**, **type-safe config extension**, **auto-cleanup** via `AbortSignal`, and a **third-party friendly** API so you can [build and distribute your own](/grid/plugin-development/custom-plugins.md). ## Next Steps Ready to dive in? - [Getting Started](/grid/getting-started.md): Detailed setup for Vanilla JS, React, Vue, and Angular - [Core Features](/grid/core.md): Sorting, rendering, keyboard navigation, and interactive playground - [Plugins](/grid/plugins.md): Extend with selection, filtering, grouping, and 24+ more - [API Reference](/grid/api-reference.md): Complete property, method, and event reference --- --- # Getting Started > Install @toolbox-web/grid and render your first grid in under a minute. Covers npm, CDN, ES modules, declarative HTML, and full framework integration for Vanilla JS, React, Vue, and Angular. **Using a framework?** Jump directly to [Angular](/grid/angular/getting-started.md), [React](/grid/react/getting-started.md), or [Vue](/grid/vue/getting-started.md). ## Quick Start 1. **Install the package** #### npm ```bash npm install @toolbox-web/grid ``` #### yarn ```bash yarn add @toolbox-web/grid ``` #### pnpm ```bash pnpm add @toolbox-web/grid ``` #### bun ```bash bun add @toolbox-web/grid ``` #### CDN For quick prototyping, use the UMD bundle directly: ```html ``` 2. **Import and use** ```typescript import '@toolbox-web/grid'; // registers import { queryGrid } from '@toolbox-web/grid'; // typed DOM helper ``` 3. **Add the grid to your HTML** ```html ``` :::caution[The grid needs a height] Set a height via CSS or inline style (e.g., `height: 400px`). Without it, the grid collapses to zero height and nothing will render. Use `height: auto` if you don't want virtual scrolling. **Tip:** A parent CSS Grid or Flexbox layout that stretches the grid (e.g., a flex child with `flex: 1` or a grid row sized with `1fr`) also counts as a height — you don't need to set one explicitly on `` itself. ::: ### Declarative Columns (No JavaScript) Define columns directly in HTML — great for static layouts and quick prototyping: ```html ``` Set `rows` from JavaScript or via the `rows` HTML attribute (JSON). See [Light DOM Columns](/grid/core.md#light-dom-columns) for the full attribute reference. ### Auto-Inferred Columns Skip column configuration entirely — the grid creates columns from your data: ```typescript import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('#my-grid'); grid.rows = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', active: true }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', active: false }, ]; // → Creates ID, Name, Email, and Active columns automatically ``` The grid detects types (`number`, `boolean`, `date`, `string`) from values and formats headers from field names (`firstName` → `First Name`). See [Column Inference](/grid/core.md#column-inference) for details. ## Framework Integration The grid is a standard web component that works in any JavaScript environment — you can always use the Vanilla JS approach in any framework. For React, Vue, and Angular, we also provide dedicated adapter packages that enable custom component renderers and editors. :::tip[Prefer a single `gridConfig` object] Across every framework, the recommended pattern is to describe the grid with one `gridConfig` object — columns, `features`, and `plugins` together — and pass it as a single prop/input/attribute. Keep reactive data (`rows`) separate, since it changes independently of configuration. A single config object is portable (the same `GridConfig` works in vanilla, React, Angular, and Vue), easy to share or snapshot in tests, and keeps multi-line feature config readable. Individual feature props (e.g. `selection="row"`) remain fully supported and are a fine shorthand for very small grids — but default to `gridConfig` for everything else, including any column that uses a custom renderer or editor. For framework adapters (React/Vue/Angular), the default should be `gridConfig.features` rather than wiring many feature props/inputs/directives in templates. Keep those template-level bindings as shorthand for tiny examples. ::: #### Angular For Angular projects, use the `@toolbox-web/grid-angular` adapter package for enhanced integration: ```bash # Install both packages npm install @toolbox-web/grid @toolbox-web/grid-angular ``` ```typescript title="grid.component.ts" import '@toolbox-web/grid'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { ColumnConfig } from '@toolbox-web/grid'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` `, }) export class EmployeeGridComponent { employees = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com' }, { id: 3, name: 'Carol White', email: 'carol@example.com' }, ]; columns: ColumnConfig[] = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]; } ``` That's a working grid. From here, add features as you need them — each is a one-line import plus an input binding: ```typescript title="grid.component.ts (with features)" import { GridEditingDirective } from '@toolbox-web/grid-angular/features/editing'; import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection'; import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering'; import '@toolbox-web/grid'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { ColumnConfig, CellCommitDetail } from '@toolbox-web/grid'; @Component({ selector: 'app-employee-grid', imports: [Grid, GridEditingDirective, GridSelectionDirective, GridFilteringDirective], template: ` `, }) export class EmployeeGridComponent { employees = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com' }, { id: 3, name: 'Carol White', email: 'carol@example.com' }, ]; columns: ColumnConfig[] = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, ]; // camelCase outputs deliver the unwrapped detail directly ($event is the // detail, not the native CustomEvent). Bind kebab-case (cell-commit) instead // if you need the CustomEvent for event.preventDefault(). onCellCommit(detail: CellCommitDetail) { console.log('Edited:', detail); } } ``` The adapter adds structural directives (`*tbwRenderer`, `*tbwEditor`), template-driven renderers/editors, and grid-level event outputs. See the [Angular adapter docs](/grid/angular/getting-started.md) for custom renderers, editors, and the complete API reference. ## Plain JavaScript (No Build Step) Don't use TypeScript or a bundler? The grid works with a single ` ``` To add plugins, use the all-in-one bundle and configure via `gridConfig`: ```html title="index.html (with plugins)" ``` :::tip `grid.umd.js` includes core only (~168 kB raw, ~48 kB gzipped). `grid.all.umd.js` bundles core + all plugins (~514 kB raw, ~136 kB gzipped). You can also load individual plugin UMD files (e.g. `selection.umd.js`) for finer control over bundle size. ::: ## TypeScript Support The package ships with full type definitions. Use generics on `queryGrid` to get typed row data throughout your code: ```typescript import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import type { ColumnConfig } from '@toolbox-web/grid'; interface Employee { id: number; name: string; email: string; } const grid = queryGrid('tbw-grid'); // Column config is type-checked against Employee const columns: ColumnConfig[] = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, // { field: 'typo' } ← TypeScript error! ]; // Event payloads are typed too grid.on('cell-commit', ({ row, field, value }) => { console.log(row.name, field, value); // row is Employee }); ``` ## Next Steps Now that you have the grid set up, explore: - [Core Features](/grid/core.md): Sorting, editing, keyboard navigation, and interactive playground - [Selection Plugin](/grid/plugins/selection.md): Add row, cell, or range selection - [Theming](/grid/guides/theming.md): Customize colors, spacing, and typography - [API Reference](/grid/api-reference.md): Complete property, method, and event reference --- # Core Features > Interactive playground, configuration, rendering, loading states, variable row heights, events, methods, and more for @toolbox-web/grid. This page documents built-in grid features that don't require plugins — the interactive playground, column configuration, data formatting, styling, loading states, events, methods, and more. --- ## Basic Usage ### Interactive Playground Experiment with the grid's core options in real time. Adjust the row count, toggle columns, change the fit mode, and enable or disable sortable/resizable columns. ```ts // InteractivePlaygroundDemo.astro import '@toolbox-web/grid'; import type { ColumnConfig, FitMode } from '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/editing'; type ColumnKey = 'id' | 'name' | 'active' | 'score' | 'created' | 'role'; const container = document.getElementById('interactive-playground-demo'); const grid = queryGrid('tbw-grid', container!); if (container && grid) { const allColumnDefs: Record = { id: { field: 'id', header: 'ID', type: 'number', sortable: true, resizable: true }, name: { field: 'name', header: 'Name', sortable: true, resizable: true }, active: { field: 'active', header: 'Active', type: 'boolean', sortable: true }, score: { field: 'score', header: 'Score', type: 'number', sortable: true, resizable: true }, created: { field: 'created', header: 'Created', type: 'date', sortable: true, resizable: true, }, role: { field: 'role', header: 'Role', type: 'select', sortable: true, options: [ { label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }, { label: 'Guest', value: 'guest' }, ], }, }; function generateRows(count: number) { const roles = ['admin', 'user', 'guest']; const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry']; const rows: Record[] = []; for (let i = 0; i < count; i++) { rows.push({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), active: i % 3 !== 0, score: Math.floor(Math.random() * 100), created: new Date(Date.now() - i * 86400000), role: roles[i % roles.length], }); } return rows; } let state = { rowCount: 100, columns: ['id', 'name', 'active', 'score', 'created', 'role'] as ColumnKey[], fitMode: 'stretch' as FitMode, sortable: true, resizable: true, }; function rebuild() { const columns = state.columns.map((key) => ({ ...allColumnDefs[key], sortable: state.sortable, resizable: state.resizable, })); grid.fitMode = state.fitMode; grid.gridConfig = { columns, sortable: state.sortable, resizable: state.resizable, typeDefaults: { date: { format: (val: Date) => val.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' }), }, }, features: { editing: 'dblclick' }, }; grid.rows = generateRows(state.rowCount); } // Initial render rebuild(); // Re-render on control change container.addEventListener('control-change', ((e: CustomEvent) => { const { allValues } = e.detail; state = { rowCount: allValues.rowCount as number, columns: (allValues.columns as ColumnKey[]) ?? state.columns, fitMode: (allValues.fitMode as FitMode) ?? state.fitMode, sortable: allValues.sortable as boolean, resizable: allValues.resizable as boolean, }; rebuild(); }) as EventListener); } ``` ### Keyboard Navigation The grid implements [ARIA grid keyboard patterns](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) out of the box — no configuration required: | Key | Action | |-----|--------| | | Move between cells | | Home / End | Jump to first/last cell in row | | Ctrl + Home / Ctrl + End | Jump to first/last cell in grid | | PgUp / PgDn | Scroll by viewport height | | ↵ Enter | Start editing (with EditingPlugin) | | Esc | Cancel editing | | ⇥ Tab / ⇧ Shift + ⇥ Tab | Move to next/previous editable cell | For the full keyboard shortcut reference, see the [Accessibility guide](/grid/guides/accessibility.md). ### RTL (Right-to-Left) Support The grid fully supports RTL languages like Hebrew, Arabic, and Persian. Set `dir="rtl"` on the grid or any ancestor element — keyboard navigation, column pinning, and layout all adapt automatically. ```html ``` ```ts // RtlDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const container = document.getElementById('rtl-demo-container'); const grid = queryGrid('#demo-rtl'); if (container && grid) { grid.columns = [ { field: 'name', header: 'الاسم', width: 150 }, { field: 'department', header: 'القسم', width: 130 }, { field: 'salary', header: 'الراتب', width: 120, formatter: (v: number) => `${v.toLocaleString('ar-EG')} ر.س` }, ]; grid.rows = [ { name: 'أحمد', department: 'الهندسة', salary: 42000 }, { name: 'فاطمة', department: 'التصميم', salary: 38000 }, { name: 'خالد', department: 'المبيعات', salary: 35000 }, { name: 'سارة', department: 'الهندسة', salary: 44000 }, { name: 'محمد', department: 'الدعم', salary: 31000 }, ]; container.addEventListener('control-change', ((e: CustomEvent) => { grid.dir = e.detail.allValues.rtl ? 'rtl' : 'ltr'; }) as EventListener); } ``` **Logical column pinning:** Use `pinned: 'start'` and `pinned: 'end'` instead of `'left'`/`'right'` for direction-independent pinning. See the [Pinned Columns plugin](/grid/plugins/pinned-columns.md) for details. --- ## Configuration ### Column Inference **Zero-config data display** — Just pass your data and the grid figures out the rest. When you provide `rows` without defining `columns`, the grid automatically: - Detects fields from the first row's property names - Infers data types (`string`, `number`, `boolean`, `date`) from actual values - Generates human-readable headers from field names (`firstName` → `First Name`) - Applies appropriate sorting and formatting for each type ```typescript grid.rows = myData; // That's it! ``` ```ts // ColumnInferenceDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('#demo-column-inference'); if (grid) { grid.rows = [ { id: 1, firstName: 'Alice', lastName: 'Johnson', age: 32, active: true, startDate: '2022-03-15' }, { id: 2, firstName: 'Bob', lastName: 'Smith', age: 28, active: false, startDate: '2023-01-10' }, { id: 3, firstName: 'Carol', lastName: 'Williams', age: 45, active: true, startDate: '2021-07-22' }, { id: 4, firstName: 'David', lastName: 'Brown', age: 36, active: true, startDate: '2020-11-05' }, { id: 5, firstName: 'Eve', lastName: 'Davis', age: 29, active: false, startDate: '2024-02-18' }, ]; } ``` #### Merge mode — infer everything, customize one column By default (`columnInference: 'auto'`), inference is **all-or-nothing**: the moment you declare a single column, the grid renders **only** that column and skips inference entirely. Opt into `columnInference: 'merge'` for a low-config workflow: the grid always infers the full column set from your data, then **overlays** any explicitly provided columns matched by `field`. Feed it data and it renders everything; if you disagree with how one column is rendered, add config for _just that column_. ```typescript grid.columnInference = 'merge'; grid.rows = employees; // every field renders, in data-key order // Customize only the salary column — the rest stay inferred: grid.columns = [{ field: 'salary', type: 'number', header: 'Salary (USD)' }]; ``` Behaviour in `merge` mode: - All data fields render in **data-key order** (from the first row), auto-typed. - A provided column overlays **only its own field** (your config wins; inferred values such as `header`/`type` fill the gaps) and keeps its data position. - A provided column for a field **absent from the data** is appended at the end as a computed column (e.g. an `actions` column). - To hide a column, omit its key from the row objects (control via the data shape). Set it via the `columnInference` prop, the `column-inference="merge"` attribute, or `gridConfig.columnInference`. The default `'auto'` preserves the classic "declare a subset → show only that subset" behaviour. ### Light DOM Columns **Declarative configuration** — Define columns in HTML instead of JavaScript. Use `` elements (or framework wrapper components) to declaratively define columns directly in your markup: #### Angular ```html ``` This approach is ideal for: - **Static layouts** where columns don't change at runtime - **Server-rendered pages** where HTML is generated on the server - **Template-driven frameworks** like Angular or Vue that prefer declarative syntax - **Quick prototyping** without writing JavaScript ```ts // LightDomColumnsDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('#demo-light-dom-columns'); if (grid) { grid.rows = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Sales' }, { id: 3, name: 'Carol Williams', email: 'carol@example.com', department: 'Marketing' }, { id: 4, name: 'David Brown', email: 'david@example.com', department: 'Engineering' }, { id: 5, name: 'Eve Davis', email: 'eve@example.com', department: 'HR' }, ]; } ``` #### Initial column ordering with the `order` attribute Use the `order` attribute to control the initial position of columns when they are first rendered. This is useful for reordering columns declaratively without JavaScript: #### Angular ```html ``` Columns without an `order` attribute keep their relative order; columns with `order` are inserted at their target indices. The `order` attribute only affects the **initial render**; user interactions (drag-to-reorder via the [Reorder plugin](/grid/plugins/reorder-columns.md)) take precedence after that. Use [`resetColumnOrder()`](/grid/api/plugins/reorder-columns/methods/resetcolumnorder/) to restore the order-attribute positioning. **Combining with `columnInference: 'merge'`** — The `order` attribute becomes especially powerful in `merge` mode. With inference enabled, the grid automatically displays all data fields, and you can use light-DOM `` elements to customize **only the columns you care about** — adding a custom `header`, overriding the `type`, adjusting `width`, and positioning via `order`, all without declaring the entire column set. ### Configuration Reference The grid is configured through the `gridConfig` property (or individual shorthand properties). The table below covers the most common options — see [`GridConfig`](/grid/api/core/interfaces/gridconfig.md) for the full, type-checked reference. | Property | Type | Description | |----------|------|-------------| | `columns` | [`ColumnConfig[]`](/grid/api/core/interfaces/columnconfig.md) | Column definitions | | `rows` | `any[]` | Row data array (top-level grid prop, not on `GridConfig`) | | `fitMode` | [`FitMode`](/grid/api/core/types/fitmode.md) | How columns fill available width (`'stretch'` or `'fixed'`) | | `columnInference` | [`ColumnInferenceMode`](/grid/api/core/types/columninferencemode.md) | How inference combines with provided columns (`'auto'` default, or `'merge'`) | | `sortable` | `boolean` | Grid-wide sort toggle (default `true`) | | `resizable` | `boolean` | Grid-wide resize toggle (default `true`) | | `initialSort` | `{ field, direction }` | Sort applied on first render (`'asc'` or `'desc'`) | | `rowHeight` | `number \| (row, index) => number \| undefined` | Fixed or variable row heights | | `getRowId` | `(row) => string` | Unique row identity function | | `rowClass` | `(row) => string \| string[]` | Dynamic row CSS classes | | `typeDefaults` | `Record` | Default format/renderer per column `type` | | `plugins` | [`GridPlugin[]`](/grid/api/plugin-development/interfaces/gridplugin.md) | Plugin instances | | `features` | [`Partial`](/grid/api/core/interfaces/featureconfig.md) | Declarative feature config (alternative to `plugins`) | | `columnState` | [`GridColumnState`](/grid/api/core/interfaces/gridcolumnstate.md) | Saved column state to restore on init | | `icons` | [`GridIcons`](/grid/api/core/interfaces/gridicons.md) | Grid-wide icon overrides | | `animation` | [`AnimationConfig`](/grid/api/core/interfaces/animationconfig.md) | Animation defaults (expand/collapse, reorder, etc.) | | `loadingRenderer` | [`LoadingRenderer`](/grid/api/core/types/loadingrenderer.md) | Custom loading indicator | | `emptyRenderer` | [`EmptyRenderer`](/grid/api/core/types/emptyrenderer.md) ` \| null` | Custom no-rows message (set `null` to suppress) | | `emptyOverlay` | `'rows' \| 'grid'` | Where to mount the empty overlay (default `'rows'`) | | `sortHandler` | [`SortHandler`](/grid/api/core/types/sorthandler.md) | Low-level sort engine override (prefer `sortComparator` per-column) | | `gridAriaLabel` | `string` | Accessible label for the grid (`aria-label`) | | `gridAriaDescribedBy` | `string` | ID of an element that describes the grid (`aria-describedby`) | | `a11y` | [`A11yConfig`](/grid/api/core/interfaces/a11yconfig.md) | Screen reader announcement messages and toggle | **Precedence (low → high):** 1. `gridConfig` prop (base) 2. Light DOM elements (declarative) 3. `columns` prop (direct array) 4. Inferred columns (auto-detected from first row) 5. Individual props (`fitMode`) — highest > In `columnInference: 'merge'` mode the order differs: the grid infers the full column set first, > then overlays the merged provided columns by `field` (provided wins, in data-key order). ### System Columns Some columns exist to support grid behaviour rather than to display user data — a row-action menu, a status indicator, a row number, the selection checkbox the grid injects for you. Mark any column with `utility: true` and the grid treats it as a **system column**: rendered normally, but excluded from chooser, reorder, print, export, clipboard, and selection. ```ts { field: '__actions', header: '', width: 80, utility: true, // ← marks this as a system column resizable: false, sortable: false, filterable: false, viewRenderer: ({ row }) => createActionsButton(row), } ``` **What `utility: true` does:** | Surface | Behaviour | | --- | --- | | Visibility panel | Not listed — users cannot toggle it on/off | | Column reorder | Locked in place | | Print | Hidden by `PrintPlugin` (override with `printHidden: false`) | | Clipboard copy | Skipped by `ClipboardPlugin` | | Export (CSV/JSON/XLSX) | Skipped by `ExportPlugin` | | Range / row selection | Click does not extend selection | | Filter UI | No filter button, no filter model entry | | Cell rendering | **Rendered normally** — your renderer runs | > **Naming convention:** Prefix the field with `__` (e.g. `__actions`, `__status`) so it cannot collide with a real data field. **Built-in system columns** the grid synthesises automatically use the same flag: `SelectionPlugin` checkbox (`__tbw_checkbox`), `MasterDetailPlugin` / `TreePlugin` / `GroupingRowsPlugin` expander (`__tbw_expander`), `RowDragDropPlugin` drag handle. **Related flags** when you want finer control: `lockPosition` (reorder only), `lockVisible` (chooser only), `printHidden` (print only), `hidden` (entirely hidden). --- ## Presentation ### Value Accessors **Resolve a cell's value when it isn't a plain field read.** By default the grid reads `row[field]`. A `valueAccessor` is only relevant when that's not the right value — typically because the cell is computed from multiple row fields, plucked from a nested structure, or derived from something outside the row. You _could_ achieve the visual result with a custom `renderer` or `format` function alone, but you'd lose every other feature that depends on knowing what the cell's value actually is — sorting (unless you also write a `sortComparator`), filtering (unless you also write a `filterValue`), grouping, aggregations, copy-to-clipboard, and exports (CSV / Excel) would all see `undefined` or the raw `row[field]`. `valueAccessor` plugs the value in once and every consumer stays consistent. ```typescript grid.columns = [ // Computed from sibling fields { field: 'total', headerName: 'Total', valueAccessor: ({ row }) => row.qty * row.price, }, // Pluck from a nested array { field: 'lastShipmentDate', headerName: 'Last Shipment', valueAccessor: ({ row }) => row.shipments?.find((s) => s.kind === 'BL')?.date, format: (v) => (v ? new Date(v).toLocaleDateString() : '—'), }, // Normalize / coerce { field: 'name', valueAccessor: ({ row }) => `${row.firstName} ${row.lastName}`.trim(), }, ]; ``` **The accessor receives** `{ row, column, rowIndex }` and returns the column's typed value. Use it whenever the *cell value* isn't a plain field read. #### Precedence For each operation, the grid looks for a column-level override first, then falls back to the accessor, then to the field: | Operation | Order | |---|---| | **Sort comparison** | `sortComparator` → `valueAccessor` → `row[field]` | | **Filter value** | `filterValue` → `valueAccessor` → `row[field]` | | **Group key & aggregations** (`sum`, `avg`, `min`, `max`, `first`, `last`) | `valueAccessor` → `row[field]` | | **Copy / export (CSV, Excel)** | `valueAccessor` → `row[field]` (then `format` if present) | | **Display** (`format` / `renderer`) | `valueAccessor` → `row[field]` | This means you write the lookup logic once and every consumer stays consistent. #### Caching & invalidation Accessor results are cached per `(row, column.field)` in a `WeakMap` keyed on the row object — so an expensive accessor (e.g. `array.find`) runs once per row regardless of how many features read it. Primitive rows bypass the cache. The cache is invalidated automatically when: - A row reference changes (immutable updates — recommended pattern). - You call `RowManager.updateRow` / `updateRows` / `applyTransaction` (in-place edits). - The Editing plugin commits a value. If you mutate row data outside of those paths, call `invalidateAccessorCache(row?, field?)` manually: ```typescript import { invalidateAccessorCache } from '@toolbox-web/grid'; row.shipments.push(newShipment); invalidateAccessorCache(row, 'lastShipmentDate'); // narrow scope // or invalidateAccessorCache(row); // all fields on this row // or invalidateAccessorCache(); // entire cache ``` :::tip[Reactive accessors (signals / refs / state)] If your accessor closes over reactive state — an Angular `signal`, a Vue `ref`, a React store value — the cache won't notice when that state changes (the row reference is still the same). Pair the accessor with a framework-level effect that invalidates the cache and asks the grid to re-render: #### Angular ```typescript import { effect, inject } from '@angular/core'; import { invalidateAccessorCache } from '@toolbox-web/grid'; fxRate = signal(1.0); columns = [{ field: 'totalUsd', valueAccessor: ({ row }) => row.totalEur * this.fxRate(), }]; constructor() { effect(() => { this.fxRate(); // tracked dependency invalidateAccessorCache(); this.grid()?.requestRender(); }); } ``` ::: #### Editing Accessors are **read-only**. Cells driven by a `valueAccessor` cannot currently be written back through the Editing plugin (a matching `valueSetter` API is planned). For now, gate them with `editable: false` or omit `editable`. --- ### Formatters **Transform how values are displayed** — Formatters convert raw data values into user-friendly text. A formatter is a function that receives the cell value and returns a display string: ```typescript grid.columns = [ // Currency { field: 'salary', format: (v) => `$${v.toLocaleString()}` }, // Date { field: 'hireDate', format: (v) => new Date(v).toLocaleDateString() }, // Percentage { field: 'progress', format: (v) => `${(v * 100).toFixed(1)}%` }, // Prefix { field: 'id', format: (v) => `#${v}` }, ]; ``` ### Row Styling **Style entire rows** based on data using the `rowClass` callback: ```typescript grid.gridConfig = { rowClass: (row) => (row.status === 'inactive' ? 'row-inactive' : ''), }; ``` Then define the CSS class in your stylesheet: ```css .row-inactive { opacity: 0.5; } ``` ### Cell Styling **Style individual cells** based on their value using `cellClass` on a column: ```typescript grid.columns = [ { field: 'score', cellClass: (value) => { if (value >= 90) return 'cell-success'; if (value < 50) return 'cell-danger'; return ''; }, }, ]; ``` :::tip You can combine `rowClass` and `cellClass`. When styles conflict, cell styles win because cells are children of rows in the DOM hierarchy. ::: ```ts // RowCellStylingDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; interface Employee { id: number; name: string; status: string; score: number; department: string; } const grid = queryGrid('#demo-row-cell-styling'); if (grid) { grid.gridConfig = { rowClass: (row) => row.status === 'inactive' ? 'row-inactive' : '', columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'department', header: 'Department', width: 120 }, { field: 'status', header: 'Status', width: 100 }, { field: 'score', header: 'Score', width: 100, align: 'right', cellClass: (value) => { if ((value as number) >= 90) return 'cell-success'; if ((value as number) < 50) return 'cell-danger'; if ((value as number) < 70) return 'cell-warning'; return ''; }, }, ], }; grid.rows = [ { id: 1, name: 'Alice', status: 'active', score: 95, department: 'Engineering' }, { id: 2, name: 'Bob', status: 'inactive', score: 42, department: 'Sales' }, { id: 3, name: 'Carol', status: 'active', score: 78, department: 'Marketing' }, { id: 4, name: 'David', status: 'active', score: 35, department: 'Engineering' }, { id: 5, name: 'Eve', status: 'active', score: 91, department: 'HR' }, { id: 6, name: 'Frank', status: 'inactive', score: 67, department: 'Finance' }, ]; } ``` ### Renderers **Full control over cell content** — Renderers let you create custom HTML elements for cells. While formatters return plain text, renderers return DOM elements. Use renderers when you need: - **Custom components**: Checkboxes, badges, progress bars, buttons - **Interactive elements**: Links, icons, action buttons - **Rich formatting**: Multiple elements, images, complex layouts #### Angular ```typescript import { Component } from '@angular/core'; import { Grid, TbwRenderer } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid, TbwRenderer], template: ` {{ value }} `, }) export class EmployeeGridComponent { rows = [/* ... */]; } ``` The renderer receives a [`CellRenderContext`](/grid/api/core/interfaces/cellrendercontext.md) — the cell value, the row object, the field name, and the column config. :::note[Why no `rowIndex`?] Renderer and editor contexts intentionally provide the **row object** instead of a row index. A `rowIndex` reflects the row's position in the grid's *current* sorted/filtered/grouped view — it silently becomes stale whenever the user sorts, filters, or reorders. The `row` object is a stable identity that remains valid regardless of view state. If you need the visual index at a specific moment (e.g. for conditional styling of even/odd rows), derive it inside the renderer: ```typescript renderer: (ctx) => { const rowIndex = grid.rows.indexOf(ctx.row); // Use rowIndex for one-time positional logic } ``` For event-driven use cases, click and activation events (`cell-click`, `row-click`, `cell-activate`) already include `rowIndex` in their detail payload. ::: ```ts // CustomRenderersDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; interface Employee { id: number; name: string; status: string; active: boolean; rating: number; salary: number; } const grid = queryGrid('#demo-custom-renderers'); if (grid) { grid.columns = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'status', header: 'Status', width: 110, renderer: ({ value, cellEl }) => { const colors: Record = { active: '#16a34a', inactive: '#dc2626', 'on-leave': '#d97706', }; const badge = document.createElement('span'); badge.style.cssText = ` display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.8em; background: ${colors[value as string] || '#888'}22; color: ${colors[value as string] || '#888'}; font-weight: 600; `; badge.textContent = (value as string).charAt(0).toUpperCase() + (value as string).slice(1); return badge; }, }, { field: 'active', header: 'Active', width: 80, align: 'center', renderer: ({ value }) => { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !!value; checkbox.disabled = true; return checkbox; }, }, { field: 'rating', header: 'Rating', width: 100, align: 'center', renderer: ({ value }) => '★'.repeat(value as number) + '☆'.repeat(5 - (value as number)), }, { field: 'salary', header: 'Salary', width: 120, align: 'right', format: (value) => `$${(value as number).toLocaleString()}`, }, ]; grid.rows = [ { id: 1, name: 'Alice Johnson', status: 'active', active: true, rating: 5, salary: 95000 }, { id: 2, name: 'Bob Smith', status: 'inactive', active: false, rating: 3, salary: 72000 }, { id: 3, name: 'Carol Williams', status: 'on-leave', active: true, rating: 4, salary: 88000 }, { id: 4, name: 'David Brown', status: 'active', active: true, rating: 5, salary: 105000 }, { id: 5, name: 'Eve Davis', status: 'active', active: false, rating: 2, salary: 65000 }, ]; } ``` #### Light DOM renderers You can also define a cell renderer **declaratively in HTML** — no JavaScript required. Nest a `` element inside a ``; its content becomes the cell template. Use `{{ value }}` to interpolate the cell value (and `{{ row.field }}` for other row fields): ```html {{ value }} ``` The companion light-DOM templates are `` (custom editor, requires the [Editing plugin](/grid/plugins/editing.md)) and `` (custom header cell). See the [API reference](/grid/api-reference.md#column-elements) for the full list. The framework adapters expose the same capability through their own template syntax — `#cell` slots in Vue, `*tbwRenderer` in Angular, and `renderer` props in React (shown in the tabs above). #### Renderer security: avoid `innerHTML` Renderers run with full DOM access. The grid does not sandbox what you do inside one, so user-supplied data must be inserted safely. The hazard is `innerHTML`: ```typescript // ✅ Safe — textContent escapes HTML automatically renderer: (ctx) => { const span = document.createElement('span'); span.textContent = ctx.value; return span; }; // ❌ XSS vulnerability — user input rendered as HTML renderer: (ctx) => { const div = document.createElement('div'); div.innerHTML = ctx.value; // If value contains ``` ```html ``` ### From SlickGrid SlickGrid's API is lower-level than the others — most behavior is composed from `Slick.Grid` + `Slick.Data.DataView` + plugin instances. Toolbox Grid bundles the equivalents into config and built-in plugins. | SlickGrid | Toolbox Grid | |-----------|--------------| | `new Slick.Grid(el, dataView, columns, options)` | `` + `grid.gridConfig = { columns, ... }` | | `Slick.Data.DataView` | Built into the grid; pass plain arrays to `grid.rows` | | `columns: [{ id, name, field }]` | `columns: [{ field, header }]` (drop separate `id`; `field` is the key) | | `dataView.setItems(rows, 'id')` | `grid.rows = rows` + `getRowId: (row) => String(row.id)` | | `dataView.sort(comparer, asc)` | `grid.sort(field, 'asc' \| 'desc')` | | `dataView.setFilter(fn)` | `FilteringPlugin` (declarative filter model) | | `Slick.Plugins.RowSelectionModel` | `features: { selection: 'row' }` | | `Slick.CheckboxSelectColumn` | `features: { selection: 'checkbox' }` | | `Slick.CellRangeSelector` + `CellSelectionModel` | `RangeSelectionPlugin` | | `onCellChange` | `cell-commit` event | | `onSelectedRowsChanged` | `selection-change` event | | Manual jQuery resize handler | Built-in column resize feature | ```html
``` ```html ``` ### From TanStack Table TanStack Table is a headless utility — you bring your own markup, virtualization, and DOM. Toolbox Grid is a rendering web component, so most of TanStack's row-model plumbing (`getSortedRowModel`, `getFilteredRowModel`, `useVirtualizer`) becomes built-in behavior or a one-line feature import. | TanStack Table | Toolbox Grid | |----------------|--------------| | `useReactTable(options)` | Not needed — render `` directly | | `columnHelper.accessor('field', …)` | Column config `{ field }` (or `valueAccessor` for computed values) | | `columnHelper.display({ cell })` | Column with a `renderer` | | `cell: (info) => …` / `flexRender` | `renderer: (ctx) => …` (returns JSX in React) | | `getSortedRowModel()` | `sortable: true` (built-in) | | `getFilteredRowModel()` | `features: { filtering: true }` (`FilteringPlugin`) | | `getGroupedRowModel()` | `features: { groupingRows: { groupOn } }` | | `getExpandedRowModel()` | `features: { tree: { childrenField } }` or `masterDetail` | | `@tanstack/react-virtual` / `useVirtualizer()` | Built-in row virtualization — no setup | | `state.sorting` / `onSortingChange` | `sort-change` event | | `state.rowSelection` / `onRowSelectionChange` | `features: { selection: 'row' }` + `selection-change` event | ```tsx // TanStack Table (React) import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender, } from '@tanstack/react-table'; const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); // …then hand-render , , with flexRender(...) ``` ```tsx // Toolbox Grid (React) import '@toolbox-web/grid-react/features/selection'; import { DataGrid, type GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns, // { field, header, sortable, renderer? } features: { selection: 'row' }, }; ; ``` ### From ngx-datatable ngx-datatable is Angular-only. Toolbox Grid's Angular adapter mirrors its declarative, template-driven style: keep `[rows]` as-is, rename column inputs, and swap `` cell templates for the `*tbwRenderer` structural directive. | ngx-datatable | Toolbox Grid | |---------------|--------------| | `` | `` with the `Grid` directive | | `[rows]` | `[rows]` (same!) | | `[columns]="[{ prop, name }]"` | `[columns]="[{ field, header }]"` | | `` | `` | | `[name]` | `header` | | `` | `*tbwRenderer="let value"` | | `[sortable]` on column | `sortable: true` on column | | `(activate)` | `(cellActivate)` / `cell-activate` event | | `[selected]` / `(select)` + `SelectionType` | `[selection]="'row'"` + `selection-change` event | | `[scrollbarV]="true"` (virtual scroll) | Built-in row virtualization — no input needed | | `[groupRowsBy]` | `GroupingRowsPlugin` (`features: { groupingRows }`) | | `[treeFromRelation]` / `[treeToRelation]` | `TreePlugin` (`features: { tree }`) | ```html ``` ```html ``` ## Get Started ```bash npm install @toolbox-web/grid ``` ```typescript import '@toolbox-web/grid'; const grid = document.createElement('tbw-grid'); grid.columns = [ { field: 'name', header: 'Name', sortable: true }, { field: 'email', header: 'Email' }, ]; grid.rows = [ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' }, ]; document.body.appendChild(grid); ``` **Ready to switch?** Check out our [Getting Started guide](/grid/getting-started.md) or [live demos](/grid/demos.md). ## See Also - [Getting Started](/grid/getting-started.md) — Install and set up your first grid in minutes - [Demos](/grid/demos.md) — Full-featured employee management demo - [Plugins Overview](/grid/plugins.md) — Complete plugin catalog with feature comparison --- # Plugins Overview > Overview of all @toolbox-web/grid plugins — editing, selection, filtering, grouping, export, and more. Tree-shakeable, individually importable. The `@toolbox-web/grid` plugin architecture allows you to extend the grid with powerful features while keeping the core bundle lightweight. Plugins are tree-shakeable — only import what you need. :::tip[Features or Plugins? They do the same thing.] **Features** and **plugins** are two APIs for enabling the same capabilities. The end result is identical — features are simply a simpler, declarative wrapper around plugins. - **Features** (recommended) — Declare _what_ you want: `features: { selection: 'row' }`. Dependencies are auto-resolved, ordering is handled for you. - **Plugins** (advanced) — Instantiate classes yourself: `plugins: [new SelectionPlugin({ mode: 'row' })]`. Useful when building custom plugins or extending `BaseGridPlugin`. **If you're getting started, use features.** You can always switch to the plugin API later if you need more control — they're fully interchangeable. ::: :::note[Angular: pair feature inputs with the per-feature directive] The Angular tabs in plugin docs often show the shorthand `[selection]`, `[filtering]`, `[editing]`, etc. on ``. In **v1.4+** the recommended way to use those bindings is to import the matching per-feature directive (e.g. [`GridSelectionDirective`](/grid/angular/api/directives/gridselectiondirective.md) from `@toolbox-web/grid-angular/features/selection`) alongside `Grid` in the component's `imports`. The same bindings on the central `Grid` directive are still accepted for v1.x backward compatibility but are `@deprecated` and will be removed in v2.0. Apps that configure plugins through `[gridConfig]="{ features: { … } }"` are unaffected — see the [Angular getting-started guide](/grid/angular/getting-started.md#three-ways-to-configure-features) for the full breakdown. ::: ## Available Plugins ### Editing & Data Entry | Plugin | Description | | ------ | ----------- | | [Editing](/grid/plugins/editing.md) | Inline cell editing with built-in editors | | [Undo/Redo](/grid/plugins/undo-redo.md) | Undo/redo for cell edits with history stack | :::note Editing is opt-in. To enable `editable` and `editor` column properties, you must enable the editing feature (`features: { editing: true }`) or register the `EditingPlugin` directly. This keeps the core bundle lightweight and provides runtime validation to catch misconfigurations early. ::: ### Selection & Interaction | Plugin | Description | | ------ | ----------- | | [Selection](/grid/plugins/selection.md) | Cell, row, and range selection with keyboard support | | [Clipboard](/grid/plugins/clipboard.md) | Copy/paste with Ctrl+C/V | | [Context Menu](/grid/plugins/context-menu.md) | Right-click context menus with submenus | | [Reorder](/grid/plugins/reorder-columns.md) | Drag-and-drop column reordering | | [Row Reorder](/grid/plugins/reorder-rows.md) | Drag-and-drop row reordering with keyboard support | ### Filtering & Sorting | Plugin | Description | | ------ | ----------- | | [Filtering](/grid/plugins/filtering.md) | Column header filters with search and dropdown | | [Multi-Sort](/grid/plugins/multi-sort.md) | Sort by multiple columns with priority indicators | ### Grouping & Hierarchy | Plugin | Description | | ------ | ----------- | | [Row Grouping](/grid/plugins/grouping-rows.md) | Group rows by field values with expand/collapse | | [Column Grouping](/grid/plugins/grouping-columns.md) | Visual column grouping with nested headers | | [Tree](/grid/plugins/tree.md) | Hierarchical tree data display | | [Master-Detail](/grid/plugins/master-detail.md) | Expandable detail rows | | [Pivot](/grid/plugins/pivot.md) | Pivot table transformation with aggregations | ### Layout & Display | Plugin | Description | | ------ | ----------- | | [Shell](/grid/plugins/shell.md) | Header bar (title + toolbar) and collapsible tool-panel sidebar | | [Pinned Columns](/grid/plugins/pinned-columns.md) | Pin columns to left or right edge | | [Pinned Rows](/grid/plugins/pinned-rows.md) | Status bar with aggregations and custom panels | | [Visibility](/grid/plugins/visibility.md) | Show/hide columns dynamically | | [Column Virtualization](/grid/plugins/column-virtualization.md) | Performance optimization for many columns | | [Responsive](/grid/plugins/responsive.md) | Card layout for narrow containers and mobile | | [Tooltip](/grid/plugins/tooltip.md) | Popover tooltips for truncated header and cell text | ### Data & Export | Plugin | Description | | ------ | ----------- | | [Export](/grid/plugins/export.md) | Export to CSV, Excel (XML), and JSON formats | | [Print](/grid/plugins/print.md) | Print-optimized layout with styling | | [Server-Side](/grid/plugins/server-side.md) | Lazy loading from remote data sources | ## Plugin Dependencies Some plugins depend on other plugins to function. Dependencies can be **hard** (required) or **soft** (optional enhancement). ### Dependency Types | Type | Behavior | Example | | ---- | -------- | ------- | | **Hard (Required)** | Plugin will throw an error if the dependency is missing | UndoRedoPlugin → EditingPlugin | | **Soft (Optional)** | Plugin works without it, but gains extra features when present | VisibilityPlugin → ReorderPlugin | ### Current Plugin Dependencies | Plugin | Depends On | Type | Reason | | ------ | ---------- | ---- | ------ | | **UndoRedoPlugin** | EditingPlugin | Hard | Tracks cell edit history for undo/redo | | **ClipboardPlugin** | SelectionPlugin | Soft | Enables copy/paste of selected cells instead of entire grid | | **VisibilityPlugin** | ReorderPlugin | Soft | Enables drag-to-reorder columns in visibility panel | ### Plugin Load Order When using the **features API** (recommended), dependencies are resolved automatically — you don't need to worry about ordering. When using the **plugin API** directly, dependencies must be loaded **before** the dependent plugin: ```typescript // ✅ Correct - EditingPlugin loaded before UndoRedoPlugin plugins: [ new EditingPlugin(), new UndoRedoPlugin(), ] // ❌ Wrong - throws error at runtime plugins: [ new UndoRedoPlugin(), // Error: EditingPlugin required new EditingPlugin(), ] ``` For declaring dependencies in your own custom plugins, see [Custom Plugins → Plugin Dependencies](/grid/plugin-development/custom-plugins.md#plugin-dependencies). ## Using Features (Recommended) The features API is the simplest and recommended way to enable grid capabilities. It provides: - **Declarative configuration** — describe _what_ you want, not _how_ to wire it up - **Automatic dependency resolution** — features auto-resolve plugin dependencies in the correct order - **Tree-shaking** via side-effect imports — only the features you import are included in your bundle - **Framework adapter integration** — React, Vue, and Angular adapters expose features as typed props #### Angular ```typescript import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection'; import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering'; import { GridEditingDirective } from '@toolbox-web/grid-angular/features/editing'; // In template: // ``` ## Feature Reference (all features, side-effect import + accepted values) One-line lookup for every feature. Import the side-effect once, then set the matching `gridConfig.features.` key (recommended) or the equivalent framework prop. Paths below use the core package; adapters mirror them — replace `@toolbox-web/grid` with `@toolbox-web/grid-react` / `-angular` / `-vue`. | Feature key | Side-effect import | Accepted values | |-------------|--------------------|-----------------| | `selection` | `@toolbox-web/grid/features/selection` | `"cell"`, `"row"`, `"column"`, `"range"`, `["column", "row"]`, `{ mode: 'range', checkbox: true }` | | `editing` | `…/features/editing` | `true`, `"click"`, `"dblclick"`, `"manual"`, `{ mode: 'row', editOn: 'click' }`, `{ mode: 'grid' }` (always-editable) | | `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: [...] }` | | `reorderColumns` | `…/features/reorder-columns` | `true` | | `reorderRows` | `…/features/reorder-rows` | `true` | | `rowDragDrop` | `…/features/row-drag-drop` | `true`, `{ dropZone: 'employees' }` | | `visibility` | `…/features/visibility` | `true` | | `pinnedColumns` | `…/features/pinned-columns` | `true` | | `pinnedRows` | `…/features/pinned-rows` | `true`, `{ slots: [{ position: 'bottom', aggregators: { … } }] }` | | `groupingColumns` | `…/features/grouping-columns` | `true`, `{ columnGroups: [...] }` | | `groupingRows` | `…/features/grouping-rows` | `{ groupOn: (row) => [row.department] }` | | `tree` | `…/features/tree` | `{ childrenField: 'children' }` | | `columnVirtualization` | `…/features/column-virtualization` | `true` | | `masterDetail` | `…/features/master-detail` | `{ detailRenderer: (row, rowIndex) => '
' }` | | `responsive` | `…/features/responsive` | `true`, `{ breakpoint: 768 }` | | `export` | `…/features/export` | `true`, `{ fileName: 'data' }` | | `print` | `…/features/print` | `true` | | `pivot` | `…/features/pivot` | `{ rowGroupFields: ['region'], columnGroupFields: ['quarter'], valueFields: [{ field: 'revenue', aggFunc: 'sum' }] }` | | `serverSide` | `…/features/server-side` | `{ dataSource: async (params) => … }` | | `tooltip` | `…/features/tooltip` | `true`, `{ header: true, cell: false }` | | `stickyRows` | `…/features/sticky-rows` | `{ isSticky: 'isSection' }`, `{ isSticky: (row) => row.isHeader, mode: 'stack', maxStacked: 3 }` | Import every feature at once (prototyping only): `import '@toolbox-web/grid/features';` ### Import All Plugins For rapid prototyping when bundle size is not critical: ```typescript import { SelectionPlugin, FilteringPlugin, EditingPlugin } from '@toolbox-web/grid/all'; ``` This imports the core grid and all 25 plugin modules. Use the `plugins` array to configure them: ```typescript grid.gridConfig = { plugins: [ new SelectionPlugin({ mode: 'row' }), new FilteringPlugin(), new EditingPlugin({ editOn: 'dblclick' }), ], }; ``` ### Accessing Plugin Instances Whether you use features or plugins, access runtime APIs the same way: ```typescript const selection = grid.getPluginByName('selection'); if (selection) { selection.selectAll(); selection.clearSelection(); const ranges = selection.getSelectedRanges(); } ``` ## Plugin Imperative API Reference (`grid.getPluginByName(name)`) `grid.getPluginByName(name)` returns the live plugin instance (or `undefined` if not loaded). The `name` is the plugin's registered key (NOT the class name). Key imperative methods per plugin: | Plugin | `name` key | Key imperative methods | |--------|-----------|------------------------| | SelectionPlugin | `selection` | `selectAll()`, `clearSelection()`, `getSelectedRanges()`, `getSelection()`, `selectRows(idx[])`, `getSelectedRowIndices()`, `getSelectedRows()`, `getSelectedColumns()` | | EditingPlugin | `editing` | (dirtyTracking) `isDirty(rowId)`, `isPristine(rowId)`, `getDirtyRows()`, `markAsPristine(rowId)`, `markAllPristine()`, `markAsDirty(rowId)`, `revertRow(rowId)`, `getOriginalRow(rowId)` | | FilteringPlugin | `filtering` | `setFilter()`, `setFilterModel()`, `clearAllFilters()`, `clearFieldFilter()` (all accept `{ silent: true }`), `getStaleFilters()`, `getBlankMode(field)`, `toggleBlankFilter(field, mode)` | | MultiSortPlugin | `multiSort` | `getSortModel()`, `setSortModel(model)`, `clearSort()`, `getSortIndex(field)`, `getSortDirection(field)` | | TreePlugin | `tree` | `expand(key)`, `collapse(key)`, `toggle(key)`, `expandAll()`, `collapseAll()`, `isExpanded(key)`, `getExpandedKeys()`, `expandToKey(key)`, `getFlattenedRows()`, `getRowByKey(key)` | | GroupingRowsPlugin | `groupingRows` | `expand(key)`, `collapse(key)`, `toggle(key)`, `expandAll()`, `collapseAll()`, `isExpanded(key)`, `getExpandedGroups()`, `getGroupState()`, `getFlattenedRows()`, `setGroups(g)`, `getGroups()`, `setGroupRows(key, rows)`, `clearGroupRows(key?)` | | GroupingColumnsPlugin | `groupingColumns` | `isGroupingActive()`, `getGroups()`, `getGroupColumns(groupId)`, `refresh()` | | MasterDetailPlugin | `masterDetail` | `expand(rowIndex)`, `collapse(rowIndex)`, `toggle(rowIndex)`, `expandAll()`, `collapseAll()`, `isExpanded(rowIndex)`, `getExpandedRows()`, `getDetailElement(rowIndex)`, `getDetailData(rowIndex)`, `isDetailLoading(rowIndex)` | | PivotPlugin | `pivot` | `enablePivot()`, `disablePivot()`, `isPivotActive()`, `getPivotResult()`, `setRowGroupFields(f)`, `setColumnGroupFields(f)`, `setValueFields(f)`, `refresh()`, `expandAll()`, `collapseAll()`, `getExpandedGroups()` | | ClipboardPlugin | `clipboard` | `copy()`, `copy({ columns, rowIndices, includeHeaders })`, `copyRows(idx[], opts)`, `getSelectionAsText(opts)`, `paste()` | | ExportPlugin | `export` | `exportCsv(opts)`, `exportExcel(opts)`, `exportJson(opts)`, `export()` (rows), `formatCsv(rows)`, `formatExcel(rows)`, `getResolvedColumns()` | | VisibilityPlugin | `visibility` | `show()`, `hide()`, `toggle()`, `isPanelVisible()`, `isColumnVisible(field)`, `setColumnVisible(field, v)`, `toggleColumn(field)`, `showAll()`, `getVisibleColumns()`, `getHiddenColumns()` | | PinnedColumnsPlugin | `pinnedColumns` | `setPinPosition(field, pos)`, `refreshStickyOffsets()`, `getLeftPinnedColumns()`, `getRightPinnedColumns()`, `clearStickyPositions()` | | PinnedRowsPlugin | `pinnedRows` | `refresh()`, `addPanel(p)`, `removePanel(id)`, `addAggregationRow(c)`, `removeAggregationRow(id)` | | ReorderPlugin | `reorderColumns` | `getColumnOrder()`, `moveColumn(field, toIndex)`, `setColumnOrder(order)`, `resetColumnOrder()` | | RowDragDropPlugin | `rowDragDrop` | `moveRow(fromIndex, toIndex)`, `canMoveRow(fromIndex, toIndex)` | | ColumnVirtualizationPlugin | `columnVirtualization` | `getIsVirtualized()`, `getVisibleColumnRange()`, `scrollToColumn(idx)`, `getTotalWidth()` | | PrintPlugin | `print` | `isPrinting()`, `print()`, `print({ orientation, title, maxRows })` | | UndoRedoPlugin | `undoRedo` | `undo()`, `redo()`, `canUndo()`, `canRedo()`, `clearHistory()`, `getUndoStack()`, `getRedoStack()`, `recordEdit(idx, field, old, new)`, `beginTransaction()`, `endTransaction()` | Inter-plugin queries (`grid.query(name, ...)`) — used when one plugin reads another without a hard dependency. SelectionPlugin exposes: `'getSelection'`, `'selectRows'`, `'getSelectedRowIndices'`, `'getSelectedRows'`, `'getSelectedColumns'`. ## Using Plugins (Advanced) For building custom plugins or when you need to instantiate plugin classes yourself (e.g., to pass constructor-only options or extend `BaseGridPlugin`), use the plugin API directly. The result is identical to using features — this is just a different way to configure the same capabilities. ### Import Paths The grid package provides multiple entry points for different use cases: | Entry Point | What It Includes | Tree-Shaking | | ----------- | ---------------- | ------------ | | `@toolbox-web/grid` | Core grid only (auto-registers ``) | N/A | | `@toolbox-web/grid/all` | Core + **all** plugins bundled | No | | `@toolbox-web/grid/plugins/*` | Individual plugin (e.g., `/plugins/selection`) | Yes | > **Important:** Do not import from both `@toolbox-web/grid` and `@toolbox-web/grid/all` in the same application. The `all` entry point already includes the core grid, so importing both will register the custom element twice. #### Plugin Import Patterns **For production apps using plugin API directly (best tree-shaking):** ```typescript // Import core grid import '@toolbox-web/grid'; // Import only the plugins you need import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection'; import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering'; import { EditingPlugin } from '@toolbox-web/grid/plugins/editing'; ``` **For prototyping (includes core grid + all plugins):** ```typescript // Import everything at once (includes core grid + all plugins) import { SelectionPlugin, FilteringPlugin, EditingPlugin } from '@toolbox-web/grid/all'; ``` ### Registration Pass plugin instances to the `gridConfig.plugins` array: ```typescript import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [...], plugins: [ new SelectionPlugin({ mode: 'row' }), new FilteringPlugin({ debounceMs: 300 }), ], }; ``` ## Plugin Configuration Each plugin accepts a configuration object: ```typescript // Selection plugin options new SelectionPlugin({ mode: 'row', multiSelect: true, checkbox: true, }); // Filtering plugin options new FilteringPlugin({ debounceMs: 200, caseSensitive: false, }); // Export plugin options new ExportPlugin({ fileName: 'grid-export', includeHeaders: true, onlyVisible: true, }); ``` ## Creating Custom Plugins The grid's plugin system lets you build fully custom functionality. Plugins extend [`BaseGridPlugin`](/grid/api/plugin-development/classes/basegridplugin.md) and can hook into lifecycle events, process rows/columns, handle keyboard events, and inject CSS. For the full development guide including lifecycle hooks, plugin communication, the query system, and styling patterns, see [Writing Custom Plugins](/grid/plugin-development/custom-plugins.md). ## Known Incompatibilities Some plugin combinations conflict and should not be used together. A development-mode warning is shown when conflicts are detected: | Plugin A | Plugin B | Reason | |----------|----------|--------| | GroupingRowsPlugin | TreePlugin | Both transform the entire row model in different ways | | GroupingRowsPlugin | PivotPlugin | Pivot creates its own aggregated row/column structure | | TreePlugin | PivotPlugin | Pivot replaces the data structure; tree hierarchy cannot coexist | | ServerSidePlugin | GroupingRowsPlugin | Grouping needs the full dataset; server-side loads blocks lazily | | ServerSidePlugin | TreePlugin | Tree needs the full hierarchy; server-side cannot provide children on demand | | ServerSidePlugin | PivotPlugin | Pivot needs the full dataset for aggregation | ## See Also - [Common Patterns](/grid/guides/common-patterns.md) — Real-world recipes combining plugins (all frameworks) - [Getting Started](/grid/getting-started.md) — Quick setup with features API - [Custom Plugin Development](/grid/plugin-development/custom-plugins.md) — Build your own plugins - [Architecture](/grid/architecture.md) — How the plugin system fits into the grid's internals - [Performance Guide](/grid/guides/performance.md) — Bundle optimization with tree-shakeable features --- # Accessibility > How @toolbox-web/grid implements WAI-ARIA grid patterns, keyboard navigation, screen reader support, and high contrast mode. `@toolbox-web/grid` follows the [WAI-ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) to provide an accessible data grid experience. This page documents the ARIA attributes, keyboard interactions, and best practices. ## Our accessibility commitment We target **WCAG 2.1 Level AA** out of the box, with no configuration required. That means: - **Every interactive operation** (sort, filter, select, edit, expand, reorder, resize, group, paste) is reachable from the keyboard alone — no mouse-only paths. - **Every state change** (sort applied, filter cleared, row selected, edit committed) is announced to assistive technology via the grid's built-in `aria-live` region. - **Every visual signal** (focus, selection, error state, sort direction) has a non-color cue (icon, text, ARIA attribute) so the grid works without color perception. - **Windows High Contrast / forced-colors** is supported automatically — the grid maps every themable surface to system colors via `@media (forced-colors: active)`. - **Reduced motion** is respected — animations, expand/collapse transitions, and scroll smoothing disable under `prefers-reduced-motion: reduce`. - **Focus is never lost** — after editing, deleting, sorting, or filtering, the grid restores focus to a sensible cell. See the [full WCAG 2.1 AA conformance table](#wcag-compliance-checklist) below for the criteria we test against. > **Filing accessibility issues:** If you find an a11y bug, please open an issue with the screen reader / browser / OS you used and what was announced (or wasn't). A11y bugs are treated as P0. ## ARIA Roles & Attributes The grid applies the following ARIA roles and attributes automatically: ### Grid Structure | Element | Role | Attributes | |---------|------|-----------| | `` | `grid` (or `treegrid` when [Tree](/grid/plugins/tree.md) or [Row Grouping](/grid/plugins/grouping-rows.md) is active) | `aria-rowcount`, `aria-colcount`, `aria-multiselectable` | | Header container | `rowgroup` | — | | Header row | `row` | `aria-rowindex="1"` | | Header cell | `columnheader` | `aria-sort`, `aria-colindex` | | Body container | `rowgroup` | — | | Data row | `row` | `aria-rowindex`, `aria-selected`, `aria-expanded`, `aria-level` / `aria-setsize` / `aria-posinset` (under `treegrid`) | | Data cell | `gridcell` | `aria-colindex`, `aria-selected`, `aria-readonly` | ### Dynamic Attributes | Attribute | Applied When | Values | |-----------|-------------|--------| | `aria-sort` | Column is sorted | `ascending`, `descending`, `none` | | `aria-selected` | Row or cell is selected | `true`, `false` | | `aria-expanded` | Row grouping/tree is active (rows with children) | `true`, `false` | | `aria-label` | `gridAriaLabel` is set, or `shell.header.title` provides a fallback (suppressed when `gridAriaLabelledBy` is set) | string | | `aria-labelledby` | `gridAriaLabelledBy` is set | id-ref | | `aria-describedby` | `gridAriaDescribedBy` is set | id-ref | | `aria-roledescription` | `gridAriaRoleDescription` is set (overrides the AT-announced role name; use sparingly — value should still describe a grid widget) | string | | `aria-level` | [Tree](/grid/plugins/tree.md) or [Row Grouping](/grid/plugins/grouping-rows.md) plugin is active (1-based hierarchical depth) | `1`, `2`, … | | `aria-setsize` | [Tree](/grid/plugins/tree.md) or [Row Grouping](/grid/plugins/grouping-rows.md) plugin is active (sibling count at this level) | integer | | `aria-posinset` | [Tree](/grid/plugins/tree.md) or [Row Grouping](/grid/plugins/grouping-rows.md) plugin is active (1-based position among siblings) | integer | | `aria-multiselectable` | Selection plugin allows multi-select | `true` | | `aria-readonly` | Cell is not editable | `true` | | `aria-rowindex` | Always (1-based) | Row position in full dataset | | `aria-colindex` | Always (1-based) | Column position | ## Keyboard Navigation The grid implements full keyboard navigation following the WAI-ARIA grid pattern: ### Basic Navigation | Key | Action | |-----|--------| | / | Move focus between rows | | / | Move focus between cells | | Home | Move to first cell in row | | End | Move to last cell in row | | Ctrl + Home | Move to first cell in grid | | Ctrl + End | Move to last cell in grid | | PgUp | Scroll up one viewport | | PgDn | Scroll down one viewport | | ⇥ Tab | Move to next cell (wraps to next row) | | ⇧ Shift + ⇥ Tab | Move to previous cell (wraps to previous row) | ### Plugin-Specific Shortcuts Keyboard shortcuts that depend on a plugin being installed are documented on each plugin's own page — that's the source of truth and stays in sync with the implementation: - [**Selection**](/grid/plugins/selection.md#keyboard-shortcuts) — Space, Shift + arrows / PgUp / PgDn / Ctrl + Home/End, Ctrl + A, Esc - [**Editing**](/grid/plugins/editing.md#keyboard-shortcuts-row-mode) — Enter (start row edit / commit), F2 (single-cell edit), Tab / Shift + Tab, Esc (cancel) - [**Clipboard**](/grid/plugins/clipboard.md#keyboard-shortcuts) — Ctrl/Cmd + C / X / V - [**Context Menu**](/grid/plugins/context-menu.md) — Shift + F10 or the dedicated ☰ Menu key opens the menu at the focused cell; / navigates, Enter/Space activates, Esc closes - [**Row Grouping**](/grid/plugins/grouping-rows.md) — Space toggles expand/collapse on a group or tree node ## Focus Management ### Focus Indicators The grid uses visible focus indicators that meet WCAG 2.1 Level AA requirements: ```css tbw-grid { /* Customize focus ring */ --tbw-color-focus-ring: #2563eb; } ``` The focus ring is 2px solid and uses `outline` (not `border`) so it doesn't affect layout. ### Focus Trapping When editing a cell, focus is trapped within the editor until the user commits (Enter/Tab) or cancels (Escape). Overlay editors (date pickers, dropdowns) use `registerExternalFocusContainer()` to extend the focus trap. ### Roving Tabindex The grid uses a roving tabindex pattern: - Only the currently focused cell has `tabindex="0"` - All other cells have `tabindex="-1"` - This means **Tab** moves focus out of the grid, and **Shift+Tab** moves focus back to the last focused cell This matches the [WAI-ARIA grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) — a grid is *one* tab stop in the page's tab order. Once focus enters, arrow keys navigate. ### Focus restoration After every operation that destroys and recreates row DOM (sort, filter, edit-commit, row removal, expand/collapse, virtualization scroll-back), the grid restores focus to the most logical cell — the same row by identity if it still exists, otherwise the same row index, otherwise the nearest surviving row in the same column. You do not need to do anything for this to work — it's automatic. ### External focus containers Overlay UIs that float *outside* the grid's DOM (date pickers, dropdowns, autocompletes used by editors or custom renderers) need to be registered so the grid's focus trap and outside-click handling treat them as part of the grid: ```ts const cleanup = grid.registerExternalFocusContainer(popoverEl); // ...later, when the popover closes: cleanup(); ``` Without this, clicking inside the popover commits the active edit and closes it. With it, the popover is treated as an extension of the focused cell. ## Screen Reader Support ### Labels and Descriptions Provide accessible labels for screen readers: ```html ``` Or reference a visible heading: ```html

Employees

``` ### Live Regions The grid uses `aria-live` regions to announce dynamic changes to screen readers: - Sort changes ("Sorted by Name, ascending") - Filter changes ("Filter applied on Name", "All filters cleared") - Group expand/collapse ("Group Engineering expanded, 5 rows") - Selection changes ("3 rows selected") - Editing lifecycle ("Editing row 1", "Row 1 saved") ### Configuring Announcements You can disable or customize live region announcements via the [`a11y`](/grid/api/core/interfaces/a11yconfig.md) config — see [`A11yMessages`](/grid/api/core/interfaces/a11ymessages.md) for the full list of overridable messages and their signatures: ```typescript // Disable all announcements grid.gridConfig = { a11y: { announcements: false }, }; // Override messages for internationalization grid.gridConfig = { a11y: { messages: { sortApplied: (col, dir) => `Trié par ${col}, ${dir}`, sortCleared: () => 'Tri effacé', filterApplied: (col) => `Filtre appliqué sur ${col}`, filterCleared: (col) => `Filtre effacé de ${col}`, allFiltersCleared: () => 'Tous les filtres effacés', groupExpanded: (name, count) => `Groupe ${name} développé, ${count} lignes`, groupCollapsed: (name) => `Groupe ${name} réduit`, selectionChanged: (count) => `${count} lignes sélectionnées`, editingStarted: (rowIndex) => `Édition de la ligne ${rowIndex + 1}`, editingCommitted: (rowIndex) => `Ligne ${rowIndex + 1} enregistrée`, dataLoaded: (count) => `${count} lignes chargées`, }, }, }; ``` Only override the messages you need — defaults (English) are used for the rest. ### Column Headers Column headers include: - Column name via text content - Sort direction via `aria-sort` - Filter state via `aria-description` ("Filtered") ### Testing with screen readers The grid is tested against NVDA, VoiceOver, and JAWS in CI-adjacent environments. When testing in your own app, here's what a screen-reader user should hear for the core flows. If your output diverges substantially from this, that's likely an a11y bug worth filing. **NVDA (Windows, Firefox or Chrome):** 1. Download [NVDA](https://www.nvaccess.org/download/) (free) and start it. 2. Tab into the grid — you should hear `", grid, rows, columns"` followed by the focused cell. 3. Press — `", column , row of "`. 4. Press Enter on a header — `"sorted ascending"` / `"sorted descending"` (and the announcement is repeated via the live region). 5. Use NVDA's *Browse Mode* toggle (Insert+Space) to switch to virtual cursor — table-reading shortcuts (Ctrl+Alt+arrows) should walk the grid as a native HTML table. **VoiceOver (macOS, Safari):** 1. Enable VoiceOver: +F5. 2. Use VO+ to enter the grid — you should hear the label and dimensions. 3. VO+Shift+ enters interaction mode; then arrow keys navigate cells with full column + row context. 4. Rotor (VO+U) → *Tables* should list the grid for jump-navigation. **JAWS (Windows):** - Use *Virtual PC Cursor* (default) for browse, *Forms Mode* (Enter) for grid interaction. Table layer (Ctrl+Alt+arrows) walks cells. **iOS VoiceOver / Android TalkBack:** - Touch a cell — the column header, value, row/column position, and any selection/expand state are announced. - Swipe right/left moves through cells; swipe up/down navigates by row. **What you should *not* hear** — these would indicate a bug worth filing: - Empty cell announcements (`"blank"`) when the cell has visible content - Position announced as `"row 1 of 1"` when there are clearly more rows (virtualization metadata is wrong) - Sort/filter changes never announced (live region not wired up) - Editing committed without any feedback ## Reduced Motion The grid respects the `prefers-reduced-motion: reduce` user preference automatically. When set, the grid: - **Disables row animations** ([row animation API](/grid/core.md#row-animation) skips transitions) - **Disables expand/collapse transitions** in tree, grouping, and master-detail plugins - **Disables scroll smoothing** in programmatic `scrollToRow()` / `scrollToCell()` calls (jumps instead of animating) - **Disables loading-state crossfade** (instant swap instead) You don't need to do anything to opt in. If you have custom renderers or plugins that animate, honor the preference: ```css @media (prefers-reduced-motion: reduce) { .my-cell-animation { transition: none; animation: none; } } ``` ## High Contrast Mode ### Using CSS Custom Properties The grid's CSS variable system makes high contrast easy: ```css /* High contrast theme */ tbw-grid.high-contrast { --tbw-color-bg: #000; --tbw-color-fg: #fff; --tbw-color-border: #fff; --tbw-color-header-bg: #1a1a1a; --tbw-color-row-hover: #333; --tbw-color-focus-ring: #ffff00; --tbw-color-accent: #00ffff; } ``` ### Pre-built Contrast Theme ```typescript import '@toolbox-web/grid/themes/dg-theme-contrast.css'; ``` ### Windows High Contrast Mode The grid ships with a built-in `@media (forced-colors: active)` block that remaps all theming variables to Windows system colors (`CanvasText`, `Canvas`, `Highlight`, `HighlightText`). Focus rings and selected rows are also overridden to use `Highlight`. No extra configuration needed — it works automatically. If you write custom renderers, follow the same pattern so your cells stay legible under forced-colors: ```css .my-status-badge { background: var(--tbw-color-accent); color: var(--tbw-color-on-accent); border: 1px solid transparent; } @media (forced-colors: active) { .my-status-badge { background: Canvas; color: CanvasText; border: 1px solid CanvasText; /* outline so the badge stays distinguishable */ forced-color-adjust: none; /* opt out of the browser's automatic remap */ } } ``` Key rules: prefer **system color keywords** (`CanvasText`, `Canvas`, `LinkText`, `ButtonText`, `Highlight`, `HighlightText`, `Mark`, `MarkText`) inside `forced-colors` media queries, and never rely on color alone — always pair with a border, underline, or icon. ## Accessibility-First Patterns These patterns are how we keep the grid accessible — and how you should keep your customizations accessible too. ### 1. Never replace content with color alone When you write a `renderer` for status, error, or category cells, give it text or an icon — not just a colored dot. The grid's built-in renderers (selection checkbox, sort indicator, expand chevron, filter button) all follow this rule. ```ts // ❌ Color-only — invisible to screen readers, fails in forced-colors { field: 'status', renderer: ({ value }) => `` } // ✅ Text + color — readable everywhere { field: 'status', renderer: ({ value }) => `${value}` } ``` ### 2. Don't suppress focus indicators The default focus ring is 2px solid `var(--tbw-color-focus-ring)` using `outline` (not `border`) so it doesn't reflow. **Never** set `outline: none` without providing an equivalent indicator — WCAG 2.4.7 fails immediately, and forced-colors users lose the only signal they have. ### 3. Use semantic ` ` ``` A bare emoji or icon has no name; screen readers announce `"button"` and the user can't tell them apart. ### 5. Don't override keyboard handling without consulting the WAI-ARIA pattern The grid implements the [WAI-ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/). If you stop event propagation in a `keydown` handler on a cell, you may break navigation. If you really need to override (rare), do it only for the specific keys you care about and let the rest bubble. ## WCAG Compliance Checklist | Criterion | Level | Status | Notes | |-----------|-------|--------|-------| | 1.1.1 Non-text Content | A | ✅ | Icons have text alternatives | | 1.3.1 Info and Relationships | A | ✅ | ARIA roles convey structure | | 1.3.2 Meaningful Sequence | A | ✅ | DOM order matches visual order | | 1.4.1 Use of Color | A | ✅ | Status not conveyed by color alone | | 1.4.3 Contrast (Min) | AA | ✅ | Default theme meets 4.5:1 ratio | | 1.4.11 Non-text Contrast | AA | ✅ | Focus indicators visible at 3:1 | | 2.1.1 Keyboard | A | ✅ | All functions accessible via keyboard | | 2.1.2 No Keyboard Trap | A | ✅ | Tab exits the grid; Escape exits editors | | 2.4.3 Focus Order | A | ✅ | Logical focus order follows grid structure | | 2.4.7 Focus Visible | AA | ✅ | Clear focus indicators | | 4.1.2 Name, Role, Value | A | ✅ | ARIA attributes on all interactive elements | ## What's Automatic The grid handles most accessibility concerns out of the box: - **ARIA roles & attributes** — `role="grid"`, `role="row"`, `role="gridcell"`, `aria-rowcount`, `aria-colcount`, `aria-rowindex`, `aria-colindex` are always set. When the [Tree](/grid/plugins/tree.md) or [Row Grouping](/grid/plugins/grouping-rows.md) plugin is active, the rows-body upgrades to `role="treegrid"` and every row carries `aria-level` / `aria-setsize` / `aria-posinset` per the [WAI-ARIA Treegrid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/) so screen readers can announce hierarchical position. - **Keyboard navigation** — Arrow keys, Tab, Enter, Home/End, PgUp/PgDn all work by default (core feature, not a plugin) - **Column headers** — Always rendered with `role="columnheader"`; cannot be hidden - **Column type inference** — Types (`number`, `date`, `boolean`) are auto-detected from the first data row - **Grid label** — Auto-derived from the shell title (`` or `shell.header.title` config) - **Windows High Contrast** — `forced-colors` media query is built into core CSS - **Live announcements** — Sort, filter, selection, grouping, and editing changes are announced via `aria-live` regions (configurable via `a11y` config) - **Plugin ARIA** — Selection, editing, filtering, tree, and grouping plugins manage their own ARIA attributes (`aria-selected`, `aria-readonly`, `aria-expanded`, `aria-multiselectable`, `aria-description`) ## Developer Responsibilities These require explicit action: 1. **Provide a grid label when not using a shell** — Without ``, set `gridAriaLabel` in config, or set `gridAriaLabelledBy` to the `id` of an existing heading next to the grid (`aria-labelledby` wins per WAI-ARIA precedence and suppresses `aria-label` to avoid conflicting names), or add `aria-label` directly on the element 2. **Test with screen readers** — NVDA (Windows), VoiceOver (macOS), Orca (Linux) 3. **Don't override keyboard handling** — The grid follows WAI-ARIA patterns; custom key listeners may conflict 4. **Use the contrast theme for additional a11y** — Import `dg-theme-contrast.css` for higher contrast beyond the built-in forced-colors support ## See Also - [Selection Plugin](/grid/plugins/selection.md): Cell, row, and range selection with full keyboard support - [Editing Plugin](/grid/plugins/editing.md): Inline cell editing with Tab/Enter/Escape navigation - [Theming](/grid/guides/theming.md): CSS custom properties, high contrast mode, and visual customization - [WAI-ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/): The W3C specification this grid follows --- # Automated testing > Write reliable Playwright, Cypress, and WebdriverIO tests against @toolbox-web/grid using its stable CSS classes, data attributes, ARIA roles, and ready() / 'render' event lifecycle hooks. `@toolbox-web/grid` is built to be tested. Every surface a test script needs — CSS class names, `data-*` attributes, ARIA roles, lifecycle promises, and events — is a **stable, documented public API**. You should never have to reverse-engineer a selector from DevTools to assert on a cell. This guide shows you what those surfaces are and how to drive the grid from Playwright, Cypress, and WebdriverIO. :::tip[For manual debugging] This page is about **automated tests in CI**. If you're poking at a grid in the DevTools console to diagnose a bug, see [Troubleshooting → How to debug the grid](/grid/guides/troubleshooting.md#how-to-debug-the-grid) — same selectors, different audience. ::: ## Why the grid is testable | Surface | What you get | Stability | |---|---|---| | **CSS classes** (`GridClasses`) | `data-grid-row` (row container), `cell` (data cell), `header-row`, `header-cell`, `selected`, `editing`, `sorted-asc`, `sorted-desc`, `focused`, … | Semver-stable, `@since 0.1.1` | | **Data attributes** (`GridDataAttrs`) | `data-field="
"`, `data-row=""`, `data-group-key` | Semver-stable | | **CSS selectors** (`GridSelectors`) | Prebuilt selectors: `DATA_ROW`, `DATA_CELL`, `SELECTED_ROWS`, `EDITING_CELL`, `CELL_BY_FIELD(field)`, … | Semver-stable | | **ARIA roles + attributes** | `role="grid"`, `role="row"`, `role="gridcell"`, `role="columnheader"`, `aria-rowindex`, `aria-colindex`, `aria-sort`, `aria-selected`, `aria-expanded` | Semver-stable — also the WCAG contract | | **Lifecycle hook** | `grid.ready()` — `Promise` resolved after first render | Public API | | **Render event** | `grid.addEventListener('render', …)` — fires once per scheduler flush | Public API, `@since 2.15.0` | Locator strategy in priority order: 1. **ARIA selectors first** (`getByRole`, `aria-rowindex`, `aria-colindex`) — they match what assistive tech sees, so a passing accessibility test is also a passing functional test. 2. **`data-field` + `data-row`** for cell lookups when you know the column field and want a specific row. 3. **`GridClasses` constants** for state assertions (`.selected`, `.editing`, `.sorted-asc`). 4. **Never** depend on framework wrapper class names (React/Vue/Angular wrappers generate them; they change between builds). ## The stable selector surface ### Finding cells ```ts // All rendered data rows 'tbw-grid .data-grid-row'; // The cell at row 42, column "email" (works for any data field) 'tbw-grid .cell[data-row="42"][data-field="email"]'; // Every cell in a given column 'tbw-grid .cell[data-field="salary"]'; // Header cell for a column 'tbw-grid .header-cell[data-field="email"]'; // Currently-editing cell 'tbw-grid .cell.editing'; // Selected rows 'tbw-grid .data-grid-row.selected'; ``` ### Finding cells by ARIA (recommended for accessibility parity) ```ts // Header cell — page.getByRole('columnheader', { name: 'Email' }) 'tbw-grid [role="columnheader"]:has-text("Email")'; // Any data cell at the visible row index N (1-based, ARIA convention) `tbw-grid [role="row"][aria-rowindex="${n}"] [role="gridcell"]`; // Cell at row N, column M (both 1-based) `tbw-grid [role="row"][aria-rowindex="${n}"] [role="gridcell"][aria-colindex="${m}"]`; // Sorted columns and their direction 'tbw-grid [role="columnheader"][aria-sort="ascending"]'; 'tbw-grid [role="columnheader"][aria-sort="descending"]'; // Selected rows 'tbw-grid [role="row"][aria-selected="true"]'; ``` > **`data-row` vs `aria-rowindex`:** `data-row` is the **0-based index** of the row in the current processed data array (attached to each cell). `aria-rowindex` is the **1-based row position** for assistive tech (attached to each row element), with the header counting as row 1. Pick one and be consistent. ## Waiting for the grid The grid renders asynchronously. **Never `setTimeout`** — wait on the real signals. ### Wait for first render `grid.ready()` returns a `Promise` that resolves after the grid has mounted, registered plugins, and completed its first render. Awaitable from any test framework via `page.evaluate()` (Playwright) or equivalent. ### Test setup — make `queryGrid` available to the page Tests run their callbacks inside the **browser**, not Node, so they can't `import` from `@toolbox-web/grid` directly inside a `page.evaluate` body. You have two ways to make `queryGrid` reachable from inside the browser context — pick the one that matches how your app loads the grid: **If your app uses an ESM bundler (Vite, Webpack, Next.js, Astro, etc.)** — expose `queryGrid` on `window` once, from your app's entry file or a test-only entry: ```ts // src/test-setup.ts — loaded by your app's dev server during tests import { queryGrid } from '@toolbox-web/grid'; declare global { interface Window { queryGrid: typeof queryGrid; } } window.queryGrid = queryGrid; ``` Now `window.queryGrid('tbw-grid')` is fully typed inside every test — no `as HTMLElement & { ... }` casts. **If your app loads the [UMD bundle](/grid/getting-started.md#plain-javascript-no-build-step)** — `window.TbwGrid.queryGrid(...)` is already available, no setup needed. Substitute `window.TbwGrid.queryGrid` for `window.queryGrid` in every example below. #### Playwright ```ts await page.evaluate(async () => { const grid = window.queryGrid('tbw-grid'); await grid?.ready(); }); ``` #### Cypress ```ts cy.window().then(async (win) => { const grid = win.queryGrid('tbw-grid'); await grid?.ready(); }); ``` #### WebdriverIO ```ts await browser.execute(async () => { const grid = window.queryGrid('tbw-grid'); await grid?.ready(); }); ``` > `queryGrid` returns a fully-typed `DataGridElement` — no manual `HTMLElement & { ... }` intersections, and it's multi-version-safe (it finds the grid even if more than one major version is loaded on the page). ### Wait for a subsequent render After you trigger a sort, filter, edit, or data swap, wait for the next `'render'` event before asserting on the new DOM. #### Playwright ```ts // Wait for the next render after triggering some action async function waitForRender(page) { await page.evaluate( () => new Promise((resolve) => window.queryGrid('tbw-grid')!.addEventListener('render', () => resolve(), { once: true }) ), ); } await page.locator('[role="columnheader"]:has-text("Salary")').click(); await waitForRender(page); ``` #### Cypress ```ts // Returns a Cypress chain that yields once the next render fires Cypress.Commands.add('waitForGridRender', () => { cy.window().then( (win) => new Cypress.Promise((resolve) => win.queryGrid('tbw-grid')!.addEventListener('render', () => resolve(), { once: true }), ), ); }); cy.get('[role="columnheader"]').contains('Salary').click(); cy.waitForGridRender(); ``` #### WebdriverIO ```ts async function waitForRender(): Promise { await browser.executeAsync((done: () => void) => { window.queryGrid('tbw-grid')!.addEventListener('render', () => done(), { once: true }); }); } await $('[role="columnheader"]=Salary').click(); await waitForRender(); ``` ## Common operations ### Sort a column #### Playwright ```ts // Click the column header await page.getByRole('columnheader', { name: 'Email' }).click(); // Assert sort direction via aria-sort (works even if you customised the icon) await expect(page.getByRole('columnheader', { name: 'Email' })).toHaveAttribute( 'aria-sort', 'ascending', ); ``` #### Cypress ```ts cy.contains('[role="columnheader"]', 'Email').click(); cy.contains('[role="columnheader"]', 'Email').should('have.attr', 'aria-sort', 'ascending'); ``` #### WebdriverIO ```ts await $('[role="columnheader"]=Email').click(); await expect($('[role="columnheader"]=Email')).toHaveAttribute('aria-sort', 'ascending'); ``` ### Read a cell's text content #### Playwright ```ts const cellAt = (row: number, field: string) => page.locator(`tbw-grid .cell[data-row="${row}"][data-field="${field}"]`); await expect(cellAt(0, 'name')).toHaveText('Alice'); ``` #### Cypress ```ts const cellAt = (row: number, field: string) => cy.get(`tbw-grid .cell[data-row="${row}"][data-field="${field}"]`); cellAt(0, 'name').should('have.text', 'Alice'); ``` #### WebdriverIO ```ts const cellAt = (row: number, field: string) => $(`tbw-grid .cell[data-row="${row}"][data-field="${field}"]`); await expect(cellAt(0, 'name')).toHaveText('Alice'); ``` ### Edit a cell #### Playwright ```ts const cell = page.locator('tbw-grid .cell[data-row="0"][data-field="email"]'); await cell.dblclick(); // open the editor await expect(cell).toHaveClass(/editing/); // wait for editor to mount await cell.locator('input').fill('alice@new.com'); await cell.locator('input').press('Enter'); // commit await expect(cell).not.toHaveClass(/editing/); await expect(cell).toHaveText('alice@new.com'); ``` #### Cypress ```ts cy.get('tbw-grid .cell[data-row="0"][data-field="email"]').as('cell'); cy.get('@cell').dblclick(); cy.get('@cell').should('have.class', 'editing'); cy.get('@cell').find('input').clear().type('alice@new.com{enter}'); cy.get('@cell').should('not.have.class', 'editing'); cy.get('@cell').should('have.text', 'alice@new.com'); ``` #### WebdriverIO ```ts const cell = await $('tbw-grid .cell[data-row="0"][data-field="email"]'); await cell.doubleClick(); await expect(cell).toHaveElementClass('editing'); const input = await cell.$('input'); await input.setValue('alice@new.com'); await browser.keys('Enter'); await expect(cell).not.toHaveElementClass('editing'); await expect(cell).toHaveText('alice@new.com'); ``` ### Select a row #### Playwright ```ts // Click the row's selection checkbox (the SelectionPlugin renders it in a __tbw_checkbox column) await page .locator('tbw-grid .cell[data-row="0"][data-field="__tbw_checkbox"] input[type="checkbox"]') .check(); await expect(page.locator('tbw-grid .data-grid-row.selected')).toHaveCount(1); await expect(page.locator('tbw-grid [role="row"][aria-selected="true"]')).toHaveCount(1); ``` #### Cypress ```ts cy.get('tbw-grid .cell[data-row="0"][data-field="__tbw_checkbox"] input[type="checkbox"]').check(); cy.get('tbw-grid .data-grid-row.selected').should('have.length', 1); ``` #### WebdriverIO ```ts await $('tbw-grid .cell[data-row="0"][data-field="__tbw_checkbox"] input[type="checkbox"]').click(); await expect($$('tbw-grid .data-grid-row.selected')).toBeElementsArrayOfSize(1); ``` ### Assert the rendered row count The grid is virtualized — `.data-grid-row` only matches **visible** rows. To assert against the full filtered dataset, read `grid.rows.length` instead of counting DOM elements. #### Playwright ```ts const visibleRows = await page.locator('tbw-grid .data-grid-row').count(); const totalRows = await page.evaluate( () => window.queryGrid('tbw-grid')!.rows.length, ); expect(totalRows).toBe(100); expect(visibleRows).toBeLessThanOrEqual(totalRows); ``` #### Cypress ```ts cy.window().then((win) => { const grid = win.queryGrid('tbw-grid')!; expect(grid.rows.length).to.equal(100); }); ``` #### WebdriverIO ```ts const totalRows = await browser.execute( () => window.queryGrid('tbw-grid')!.rows.length, ); expect(totalRows).toEqual(100); ``` ## The virtualization gotcha At any moment the DOM only contains the rows in the viewport + a small overscan buffer. **If `data-row="500"` doesn't exist as a CSS match, the row hasn't been rendered yet — that's not a bug, that's virtualization working.** Bring it into view before interacting. #### Playwright ```ts // Scroll row 500 into view, then assert on it await page.evaluate( ([rowIndex]) => { window.queryGrid('tbw-grid')!.scrollToRow(rowIndex); }, [500], ); await waitForRender(page); await expect(page.locator('.cell[data-row="500"][data-field="name"]')).toBeVisible(); ``` #### Cypress ```ts cy.window().then((win) => { win.queryGrid('tbw-grid')!.scrollToRow(500); }); cy.waitForGridRender(); cy.get('.cell[data-row="500"][data-field="name"]').should('be.visible'); ``` #### WebdriverIO ```ts await browser.execute((rowIndex: number) => { window.queryGrid('tbw-grid')!.scrollToRow(rowIndex); }, 500); await waitForRender(); await expect($('.cell[data-row="500"][data-field="name"]')).toBeDisplayed(); ``` If your row has a stable business ID and you've configured [`getRowId`](/grid/api/core/interfaces/gridconfig.md#getrowid), use `scrollToRowById('emp-42')` instead — it survives sort and filter changes. ## Test against row identity, not row index `data-row="0"` is "whatever happens to be the first visible row right now". After a sort it might be a different employee; after a filter it might be empty. For stable assertions, look up the cell **by the business value** instead: #### Playwright ```ts // "The row that contains 'Alice' in the name column" const aliceRow = page.locator('tbw-grid .data-grid-row', { has: page.locator('.cell[data-field="name"]', { hasText: 'Alice' }), }); await expect(aliceRow.locator('.cell[data-field="email"]')).toHaveText('alice@example.com'); ``` #### Cypress ```ts cy.get('tbw-grid .data-grid-row') .filter(':has(.cell[data-field="name"]:contains("Alice"))') .find('.cell[data-field="email"]') .should('have.text', 'alice@example.com'); ``` #### WebdriverIO ```ts const aliceRow = await $('tbw-grid .data-grid-row*=Alice'); await expect(aliceRow.$('.cell[data-field="email"]')).toHaveText('alice@example.com'); ``` ## Anti-patterns - **`page.waitForTimeout(500)`** to "let the grid settle" — flake city. Always await `grid.ready()` or the next `'render'` event. - **Counting `.data-grid-row` elements** as a proxy for total rows — virtualization makes this wrong by design. Read `grid.rows.length`. - **Asserting on visual position** ("the third row from the top") — sorting and filtering invalidate this. Assert on data identity (the row that contains "Alice"). - **Querying by framework wrapper class names** (`._ng-content-c12`, React fiber IDs, Vue scoped attributes) — these change between builds. Stick to the grid's published classes and ARIA attributes. - **Using `effectiveConfig` / internal APIs in test assertions** — these aren't covered by semver. Use `await grid.getConfig()` if you need to inspect resolved config. ## See also - [Troubleshooting → debug APIs](/grid/guides/troubleshooting.md#how-to-debug-the-grid): The manual / DevTools-console equivalent of this guide — same selectors, different audience - [Accessibility](/grid/guides/accessibility.md): The full ARIA contract — anything documented there is also a stable test selector - [GridSelectors API](/grid/api/core/variables/gridselectors/): Typed reference for every selector / class / data-attribute the grid publishes - [Multi-version coexistence](/grid/guides/multi-version.md): Testing apps that load more than one grid version on the same page --- # Common Patterns > Practical recipes for combining grid features. Covers data browsing, editable grids, master-detail, grouping, export, and more. Real-world grids rarely use a single feature in isolation. This guide shows **tested combinations** that solve everyday requirements. ## Data Browsing & Selection **Goal:** Let users sort, filter, and select rows from a large dataset. #### Angular ```typescript 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: ` `, }) 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 } } ``` :::tip Use `getSelectedRows()` instead of `getSelectedRowIndices()` — row objects are stable identifiers even after sorting and filtering. ::: ## Editable Grid with Undo **Goal:** Inline cell editing with full undo/redo support. #### Angular ```typescript 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: ` `, }) 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 } } ``` :::tip When using the features API, plugin dependencies are resolved automatically — no need to worry about ordering. If using the advanced plugins API directly, `EditingPlugin` must be loaded **before** `UndoRedoPlugin`. ::: ## Grouped Data with Aggregates **Goal:** Group rows by a field and show aggregated values (sum, average, count). #### Angular ```typescript 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: ` `, }) 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 **Goal:** Let users filter data, select a subset, and export it. #### Angular ```typescript 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: ` `, }) 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 **Goal:** Expand a row to show related detail records. #### Angular ```typescript 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: `
Order #{{ row.orderId }}
`, }) 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 **Goal:** Handle 10k+ rows with pinned identifier columns and column virtualization. #### Angular ```typescript 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: ` `, }) 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 ]; } ``` :::tip For datasets over 50k rows, consider `ServerSidePlugin` to paginate and filter on the server instead of loading all data upfront. ::: --- ## 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. ```typescript import { createGrid } from '@toolbox-web/grid'; import type { RowTransaction } from '@toolbox-web/grid'; const grid = createGrid('#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 }], }); }; ``` ### Transaction shape ```typescript interface RowTransaction { add?: T[]; // Appended to the end update?: { id: string; changes: Partial }[]; // 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 | Scenario | Method | Animations | |----------|--------|------------| | User action, moderate stream (< 10 msg/s) | `applyTransaction()` | Yes (configurable) | | High-frequency ticker (100+ msg/s) | `applyTransactionAsync()` | Disabled (batched) | :::tip Both methods return a `TransactionResult` with the actual `added`, `updated`, and `removed` row objects — useful for logging or post-processing. ::: ## 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 The grid dispatches standard `CustomEvent`s 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](/grid/api-reference.md#cancelable-events). #### Angular ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid'; @Component({ imports: [Grid], template: ` `, }) export class EmployeeGridComponent { onCellClick(detail: CellClickDetail) { console.log(detail.row, detail.field); } onCellCommit(detail: CellCommitDetail) { if (!isValid(detail.value)) { // To cancel, use the native event via kebab-case binding: // (cell-commit)="onCellCommit($event)" → event.preventDefault() } } } ``` :::note[camelCase vs kebab-case] Use **camelCase** outputs like `(cellClick)` for typed, unwrapped detail. Use kebab-case `(cell-commit)` when you need `preventDefault()` on the native `CustomEvent`. ::: ### 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. :::tip[Server-side pagination?] For paginated server-side data, prefer the [`ServerSidePlugin`](/grid/plugins/server-side.md) — it handles block fetching out of the box. `tbw-scroll` is the lower-level primitive for cases the plugin doesn't cover (custom load-more triggers, deferred cell content, scroll-driven UI, etc.). ::: #### Infinite scroll / load more ```typescript grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => { if (scrollTop + clientHeight >= scrollHeight - 200) { loadNextPage(); } }); ``` #### Sticky scroll-progress indicator A toolbar or progress bar that lives outside the grid and tracks scroll position: ```typescript 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) Render expensive content (charts, images, embedded video) only when its row is near the viewport: ```typescript grid.on('tbw-scroll', ({ scrollTop, clientHeight }) => { // Lazy-mount components in the visible window hydrateHeavyCellsBetween(scrollTop, scrollTop + clientHeight); }); ``` #### Dismiss overlays on scroll Tooltips, popovers, context menus rendered outside the grid should typically close when the user scrolls: ```typescript grid.on('tbw-scroll', () => closeOpenOverlays()); ``` :::note[Not for per-row visibility tracking] `tbw-scroll` fires at most once per frame, **not** per row entering or leaving the viewport. For "row N just became visible" semantics, observe rendered row elements directly in `afterRowRender` or use `IntersectionObserver` from a custom plugin. ::: **Framework adapter names** — disambiguated to avoid colliding with native scroll events: | Adapter | Binding | |---------|---------| | React | `` | | Vue | `` | | Angular | `` | ### 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. :::tip[`ready()` vs `render` event] `grid.ready()` resolves **once**, after the first render. The `render` event fires on **every** flush. When you only care about a specific mutation (e.g. the row you just added), attach with `{ once: true }`. ::: 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 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. ```typescript function addEmployee() { grid.addRow({ id: crypto.randomUUID(), name: '', email: '' }); grid.addEventListener( 'render', () => { const input = grid.querySelector( '[data-row="0"][data-col="0"] input', ); input?.focus(); }, { once: true }, ); } ``` #### 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`: ```typescript 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 | `` | | Vue | `` | | Angular | `` | ## See Also - [Plugins Overview](/grid/plugins.md): Browse all 23 plugins - [Events Reference](/grid/api-reference.md#events): Complete event catalog for all plugins - [Performance](/grid/guides/performance.md): Virtualization, benchmarks, and optimization tips - [API Reference](/grid/api-reference.md): Full property and method reference --- # Migrating from v1 to v2 > Complete migration guide for upgrading @toolbox-web/grid and its framework adapters from v1 to v2. This guide covers every breaking change in `@toolbox-web/grid` v2 and its Angular, React, and Vue adapters. Most changes are simple renames that can be done with find-and-replace. A handful require manual migration due to structural API changes. ## Quick Summary v2 removes **~106 deprecated items** across the grid core and all three framework adapters. The majority are renames — removing framework prefixes from types (e.g., `ReactGridConfig` → `GridConfig`), renaming features for clarity (e.g., `sorting` → `multiSort`), and replacing `sticky` with `pinned`. A few structural API changes (like `useGridEvent()` removal) require manual attention. :::note **Most of these APIs have been deprecated for a long time.** The earliest deprecations date back to v1.0.0, and the most recent batch was introduced in v1.24.0+. If you've been following deprecation warnings in your IDE and keeping your code up to date, you may have very little (or nothing) to change. The tables below include a "Deprecated since" column so you can quickly check whether a removal affects your version. ::: :::tip Run `git diff` after each section to verify changes before committing. ::: ## Automated Find-and-Replace ~75 of ~106 changes are simple 1:1 renames. Use your IDE's find-and-replace (Ctrl+Shift+H) or the `sed` commands below. :::caution **Order matters** for some replacements. Replace `rowReorder` before `reorder`, and `row-reorder` before `reorder` in import paths, to avoid creating incorrect names like `reorderColumnsRows`. ::: ### Grid Core — Type & Event Renames | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | ------------------------ | ------------------------ | -------- | ---------------- | | `ActivateCellDetail` | `CellActivateDetail` | Type | v1.0.0 | | `activate-cell` | `cell-activate` | Event | v1.0.0 | | `StickyPosition` | `PinnedPosition` | Type | v1.15.0 | | `ResolvedStickyPosition` | `ResolvedPinnedPosition` | Type | v1.15.0 | | `EditorContext` | `ColumnEditorContext` | Type | v1.12.0 | | `RowGroupingConfig` | `GroupingRowsConfig` | Type | v1.24.0 | | `RowGroupingState` | `GroupingRowsState` | Type | v1.24.0 | | `PLUGIN_QUERIES` | String literals with `grid.query()` | Constant | v1.8.0 | ### Grid Core — Property & Attribute Renames | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | -------------- | ---------------- | -------------------- | ---------------- | | `.sticky` | `.pinned` | Column config property | v1.15.0 | | `'sticky'` | `'pinned'` | String key | v1.15.0 | | `sticky:` | `pinned:` | Config key | v1.15.0 | | `sizable` | `resizable` | HTML attribute | v1.0.0 | ### Server-Side Plugin — DataSource Renames The server-side plugin's datasource interface was renamed from row-centric to node-centric terminology (a "node" is the atomic pagination unit — a row for flat data, a top-level tree/group node for hierarchical data). | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | ---------------------------------- | ----------------------------------- | --------------------- | ---------------- | | `GetRowsParams.startRow` | `GetRowsParams.startNode` | Interface property | v1.24.0 | | `GetRowsParams.endRow` | `GetRowsParams.endNode` | Interface property | v1.24.0 | | `GetRowsResult.totalRowCount` | `GetRowsResult.totalNodeCount` | Interface property | v1.24.0 | | `GetRowsResult.lastRow` | `GetRowsResult.lastNode` | Interface property | v1.24.0 | | `plugin.getTotalRowCount()` | `plugin.getTotalNodeCount()` | Method | v1.24.0 | | `plugin.isRowLoaded(index)` | `plugin.isNodeLoaded(index)` | Method | v1.24.0 | ### Grid Core — Feature Alias Renames | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | -------------- | ------------------ | ------------ | ---------------- | | `'sorting'` | `'multiSort'` | Feature name | v1.24.0 | | `sorting:` | `multiSort:` | Config key | v1.24.0 | | `'reorder'` | `'reorderColumns'` | Feature name | v1.24.0 | | `reorder:` | `reorderColumns:` | Config key | v1.24.0 | | `rowReorder` | `reorderRows` | Feature name | v1.24.0 | ### Grid Core — Function Renames (Plugin Authors) | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | -------------------------- | ---------------------- | ----------- | ---------------- | | `onPluginQuery` | `handleQuery` | Plugin hook | v1.8.0 | | `getPivotAggregator` | `getValueAggregator` | Function | v1.24.0 | | `createPinnedRowsElement` | `createInfoBarElement` | Function | v1.24.0 | ### Angular Adapter | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | --------------------- | ------------------ | -------------------- | ---------------- | | `AngularGridAdapter` | `GridAdapter` | Type / export | 0.11.0 | | `AngularCellRenderer` | `CellRenderer` | Type | 0.11.0 | | `AngularCellEditor` | `CellEditor` | Type | 0.11.0 | | `AngularColumnConfig` | `ColumnConfig` | Type | 0.11.0 | | `AngularGridConfig` | `GridConfig` | Type | 0.11.0 | | `AngularTypeDefault` | `TypeDefault` | Type | 0.11.0 | | `[angularConfig]` | `[gridConfig]` | Directive input | 0.11.0 | | `[sorting]` | `[multiSort]` | Directive input | 0.11.0 | | `[reorder]` | `[reorderColumns]` | Directive input | 0.11.0 | | `[rowReorder]` | `[reorderRows]` | Directive input | 0.11.0 | | `TbwCellEditor` | `TbwEditor` | Structural directive | 0.11.0 | | `TbwCellView` | `TbwRenderer` | Structural directive | 0.11.0 | **Feature import paths:** | v1 path | v2 path | | ------------------------------------------------ | ------------------------------------------------------ | | `@toolbox-web/grid-angular/features/sorting` | `@toolbox-web/grid-angular/features/multi-sort` | | `@toolbox-web/grid-angular/features/reorder` | `@toolbox-web/grid-angular/features/reorder-columns` | | `@toolbox-web/grid-angular/features/row-reorder` | `@toolbox-web/grid-angular/features/reorder-rows` | ### React Adapter | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | ------------------------------------- | ------------------------------ | -------------- | ---------------- | | `ReactGridAdapter` | `GridAdapter` | Type / export | 0.11.0 | | `ReactColumnConfig` | `ColumnConfig` | Type | 0.11.0 | | `ReactGridConfig` | `GridConfig` | Type | 0.11.0 | | `ReactTypeDefault` | `TypeDefault` | Type | 0.11.0 | | `reactTypeDefaultToGridTypeDefault` | `typeDefaultToBaseTypeDefault` | Function | 0.11.0 | | `processReactGridConfig` | `processGridConfig` | Function | 0.11.0 | | `sorting` prop | `multiSort` prop | Component prop | 0.11.0 | | `reorder` prop | `reorderColumns` prop | Component prop | 0.11.0 | | `rowReorder` prop | `reorderRows` prop | Component prop | 0.11.0 | **Feature import paths:** | v1 path | v2 path | | -------------------------------------------- | -------------------------------------------------- | | `@toolbox-web/grid-react/features/sorting` | `@toolbox-web/grid-react/features/multi-sort` | | `@toolbox-web/grid-react/features/reorder` | `@toolbox-web/grid-react/features/reorder-columns` | | `@toolbox-web/grid-react/features/row-reorder` | `@toolbox-web/grid-react/features/reorder-rows` | ### Vue Adapter | v1 (removed) | v2 (replacement) | Kind | Deprecated since | | ----------------- | --------------------- | -------------- | ---------------- | | `VueGridAdapter` | `GridAdapter` | Type / export | 0.11.0 | | `VueCellRenderer` | `CellRenderer` | Type | 0.11.0 | | `VueCellEditor` | `CellEditor` | Type | 0.11.0 | | `VueColumnConfig` | `ColumnConfig` | Type | 0.11.0 | | `VueGridConfig` | `GridConfig` | Type | 0.11.0 | | `VueTypeDefault` | `TypeDefault` | Type | 0.11.0 | | `sorting` prop | `multiSort` prop | Component prop | 0.11.0 | | `reorder` prop | `reorderColumns` prop | Component prop | 0.11.0 | | `rowReorder` prop | `reorderRows` prop | Component prop | 0.11.0 | **Feature import paths:** | v1 path | v2 path | | ------------------------------------------ | ------------------------------------------------ | | `@toolbox-web/grid-vue/features/reorder` | `@toolbox-web/grid-vue/features/reorder-columns` | | `@toolbox-web/grid-vue/features/row-reorder` | `@toolbox-web/grid-vue/features/reorder-rows` | ### CSS Default Export ```typescript // v1 import styles from '@toolbox-web/grid/styles'; // v2 import { gridStyles } from '@toolbox-web/grid/styles'; ``` --- ## Manual Migration Required These structural API changes cannot be safely automated with find-and-replace. Each requires understanding the surrounding code. ### `queryPlugins()` → `query()` — Parameter format change *Deprecated since v1.8.0.* The object-based parameter was replaced with a flat signature. #### v1 (removed) ```typescript const results = grid.queryPlugins({ type: 'canMoveColumn', context: column, }); ``` #### v2 ```typescript const results = grid.query('canMoveColumn', column); ``` ### `suspendProcessing()` — Removed entirely *Deprecated since v1.21.0.* This method was already a no-op in late v1. Delete all calls to `suspendProcessing()` and `resumeProcessing()`. Use `insertRow()` / `removeRow()` for individual row mutations — batching is handled internally. #### v1 (removed) ```typescript grid.suspendProcessing(); // ... batch operations ... grid.resumeProcessing(); ``` #### v2 ```typescript // Just use row mutation methods directly grid.insertRow(newRow, index); grid.removeRow(rowId); ``` ### `getExtraHeight()` / `getExtraHeightBefore()` → `getRowHeight()` *Plugin authors only. Deprecated since v1.12.0.* The two aggregate height hooks were replaced by a single per-row hook. #### v1 (removed) ```typescript getExtraHeight(): number { return this.expandedRows.size * this.detailHeight; } getExtraHeightBefore(beforeRowIndex: number): number { let height = 0; for (const idx of this.expandedRowIndices) { if (idx < beforeRowIndex) height += this.detailHeight; } return height; } ``` #### v2 ```typescript getRowHeight(row: unknown, index: number): number | undefined { if (this.isExpanded(row)) { return this.baseRowHeight + this.getDetailHeight(row); } return undefined; // use default height } ``` ### `resolveIcon()` → `setIcon()` *Plugin authors only. Deprecated since v1.31.0.* The return-value pattern was replaced with a DOM-mutation pattern. #### v1 (removed) ```typescript const icon = this.resolveIcon('sort-asc'); element.innerHTML = typeof icon === 'string' ? icon : ''; ``` #### v2 ```typescript this.setIcon(element, 'sort-asc'); ``` ### `PLUGIN_QUERIES` constant → String literals *Plugin authors only. Deprecated since v1.8.0.* The constant object was removed. Use string literals with `grid.query()` instead. #### v1 (removed) ```typescript import { PLUGIN_QUERIES } from '@toolbox-web/grid'; grid.queryPlugins({ type: PLUGIN_QUERIES.CAN_MOVE_COLUMN, context: column, }); ``` #### v2 ```typescript grid.query('canMoveColumn', column); ``` ### Angular: `commit`/`cancel` outputs → `onCommit`/`onCancel` callbacks *Deprecated since grid-angular 0.11.0.* The `EventEmitter`-based outputs on the editor directive were replaced with simple callback functions on the editor context. #### v1 (removed) ```typescript // In your editor component @Output() commit = new EventEmitter(); @Output() cancel = new EventEmitter(); save() { this.commit.emit(this.value); } ``` #### v2 ```typescript // Editor context provides callbacks directly save() { this.context.onCommit(this.value); } discard() { this.context.onCancel(); } ``` ### React: `useGridEvent()` → Event props on `` *Deprecated since grid-react 0.18.0.* The `useGridEvent` hook was removed. Use event handler props directly on the `` component. #### v1 (removed) ```tsx import { useGridEvent } from '@toolbox-web/grid-react'; function MyGrid() { const gridRef = useRef(null); useGridEvent(gridRef, 'selection-change', (e) => { setSelected(e.detail); }); return ; } ``` #### v2 ```tsx function MyGrid() { return ( setSelected(e.detail)} /> ); } ``` ### Vue: `useGridEvent()` → Template event handlers *Deprecated since grid-vue 0.11.0.* The `useGridEvent` composable was removed. Use standard Vue template event handlers on ``. #### v1 (removed) ```vue ``` #### v2 ```vue ``` ### All Adapters: `SelectionMethods`/`ExportMethods` → Feature-specific functions *Deprecated since adapter 0.11.0.* The monolithic `injectGrid()` / `useGrid()` return type no longer includes selection and export methods. Import them from their respective feature modules instead. #### Angular ```typescript // v1 const { selectAll, clearSelection, exportToCsv } = injectGrid(); // v2 import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection'; import { injectGridExport } from '@toolbox-web/grid-angular/features/export'; const { selectAll, clearSelection } = injectGridSelection(); const { exportToCsv } = injectGridExport(); ``` --- ## FAQ ### Can I use v1 plugins with v2? Yes, as long as they use `handleQuery()` (not `onPluginQuery()`). If your plugin implements `onPluginQuery`, rename it to `handleQuery` — the signature is identical. ### Do I need to update adapter packages at the same time? Yes. The v2 adapter packages (`@toolbox-web/grid-angular`, `@toolbox-web/grid-react`, `@toolbox-web/grid-vue`) require `@toolbox-web/grid` v2 as a peer dependency. Update all packages together. ### What about the `default` CSS export? The default export from the styles module was removed. Use the named export instead: ```typescript // v1 import styles from '@toolbox-web/grid/styles'; // v2 import { gridStyles } from '@toolbox-web/grid/styles'; ``` ### Are there any runtime behavior changes? The `suspendProcessing()` method was already a no-op in late v1 — removing it has no runtime impact. All other changes are API surface renames. If your v1 code compiled without deprecation warnings, the migration is purely mechanical. --- # Multi-version coexistence > How @toolbox-web/grid lets two different grid versions live on one page — useful for micro-frontends and gradual upgrades. This guide is **framework-agnostic**. Multi-version coexistence is a property of the underlying web component (`` and its custom-element registry), not of any adapter — it applies equally to vanilla JavaScript, Module Federation, [Native Federation](https://github.com/angular-architects/module-federation-plugin/blob/main/libs/native-federation/README.md), iframe-less micro-frontend shells, and any setup where two copies of `@toolbox-web/grid` end up on the same page. You don't need a framework adapter, and you don't need to install anything extra. When two micro-frontends on the same page each ship their own copy of `@toolbox-web/grid` (for example, a host app on `v2.14` while a widget is still on `v2.11`), the browser's custom-elements registry is shared and first-wins: a second `customElements.define('tbw-grid', …)` call throws. Starting with `v2.14`, the grid handles this for you automatically — the second bundle to load registers itself under a version-suffixed tag and the two implementations coexist. ## How it works Each grid bundle exposes the version it was built with on the class: ```ts import { DataGridElement } from '@toolbox-web/grid'; DataGridElement.version; // e.g. '2.14.0' — set at build time DataGridElement.tagName; // 'tbw-grid' — the canonical tag DataGridElement.activeTag; // 'tbw-grid' OR 'tbw-grid-v' ``` At module load the bundle calls `customElements.define()` using the following rule: 1. **No existing registration** → register under `tbw-grid`. The bundle owns the bare tag. `activeTag === 'tbw-grid'`. 2. **An existing registration at the SAME version** → don't re-register; reuse the existing class. `activeTag === 'tbw-grid'`. 3. **An existing registration at a DIFFERENT version** → register under `tbw-grid-v` (e.g. `tbw-grid-v2-14-0`). `activeTag` reflects the suffixed tag. The sanitiser replaces every non-alphanumeric character with `-` and lower-cases the result, so semver pre-release / build-metadata strings like `2.14.0-beta.1+build.42` produce `tbw-grid-v2-14-0-beta-1-build-42`. In the **single-version** case (99% of apps), nothing changes: the tag is `` and existing markup, styles, and tooling continue to work unmodified. ## The `data-tbw-grid` selector contract Because the tag name is no longer guaranteed to be `tbw-grid` literally, every grid sets a stable attribute on its host element when it connects: ```html ``` **Always use `[data-tbw-grid]` (not the tag name) when you need to target "any grid" in CSS, querySelector, or `closest()`**. The framework adapters (`@toolbox-web/grid-react`, `@toolbox-web/grid-vue`, `@toolbox-web/grid-angular`) already do this internally — your application code only needs to follow the same rule for hand-written selectors: ```css /* ✅ Works for both bare and suffixed grids */ [data-tbw-grid] { font-size: 14px; } /* ⚠️ Only matches the first-loaded bundle's bare tag */ tbw-grid { font-size: 14px; } ``` The bundled themes (`@toolbox-web/grid/themes/dg-theme-*.css`) already use `[data-tbw-grid]` for their root selectors, so they apply correctly to every loaded version. If you author a custom theme, follow the same pattern. ## Adapter usage The React and Vue adapters resolve `activeTag` at render time, so they always emit the correct tag for the bundle they imported: ```tsx // React — renders or depending on which bundle // supplied DataGridElement. import { DataGrid } from '@toolbox-web/grid-react'; ``` ```vue ``` The Angular adapter's `Grid` directive matches both the bare `tbw-grid` tag **and** the stable `[data-tbw-grid]` attribute, and its shell-content directives (`tbw-grid-header-content`, `tbw-grid-toolbar-content`) locate their host via `closest('[data-tbw-grid]')`. In single-version setups embed `` literally as before. For multi-version Angular usage, render the active tag for the bundle you imported and add `data-tbw-grid` so the directive attaches (Angular matches selectors at compile time, so the runtime tag can't be matched by name alone): ```html ``` ## Per-bundle styles The internal style injector scopes its `