# @toolbox-web/grid — Full Documentation > A high-performance, framework-agnostic data grid built with pure TypeScript and native Web Components. Zero runtime dependencies. This file concatenates every prose documentation page — guides, plugins, and framework adapters — into one document for one-shot ingestion. Demo code is inlined as fenced blocks. To keep the file ingestion-friendly, plugin pages are condensed to a stub — intro, `Installation`, and `Basic Usage` — with each plugin's deep API (configuration options, events, methods) linked as a `.md` companion; fetch it by appending `.md` to the plugin's page URL. The exhaustive per-symbol TypeDoc API is likewise linked (not inlined) at the end. For the curated link index, see `llms.txt`. A few non-implementation pages (the changelogs and the AI-assistance overview) are intentionally omitted here to keep the corpus focused on building with the grid; they remain linked in `llms.txt`. --- ## Contents Every entry below appears in full further down this file under an `# ` heading — grep for that exact heading to jump to it, or fetch the single-page `.md` URL instead of the whole corpus. Plugin pages appear here as a stub (intro + install + basic usage); their deep API lives at the linked `.md`. ### Core Documentation - **Introduction** — A high-performance, framework-agnostic data grid web component for JavaScript, React, Angular, and Vue. Zero dependencies, 100k+ rows, virtualized rendering. (https://toolboxjs.com/grid/introduction.md) - **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. (https://toolboxjs.com/grid/getting-started.md) - **Core Features** — Interactive playground, configuration, rendering, loading states, variable row heights, events, methods, and more for @toolbox-web/grid. (https://toolboxjs.com/grid/core.md) - **Architecture** — Internal architecture of @toolbox-web/grid — configuration system, render scheduler, virtualization, plugin lifecycle, and light DOM design. (https://toolboxjs.com/grid/architecture.md) - **API Reference** — Complete reference for the <tbw-grid> component — properties, methods, events, CSS custom properties, keyboard shortcuts, declarative configuration, and accessibility. (https://toolboxjs.com/grid/api-reference.md) - **Error & Warning Reference** — Complete reference for all diagnostic codes emitted by @toolbox-web/grid. Each code links to an explanation and resolution steps. (https://toolboxjs.com/grid/errors.md) - **Platform & Support** — What you can rely on when you depend on @toolbox-web/grid — adapter support matrix, browser baseline, CSP & security handling, server-side rendering, deprecation policy, and release cadence. (https://toolboxjs.com/grid/platform.md) - **Plugins Overview** — Overview of all @toolbox-web/grid plugins — editing, selection, filtering, grouping, export, and more. Tree-shakeable, individually importable. (https://toolboxjs.com/grid/plugins.md) ### Guides - **Accessibility** — How @toolbox-web/grid implements WAI-ARIA grid patterns, keyboard navigation, screen reader support, and high contrast mode. (https://toolboxjs.com/grid/guides/accessibility.md) - **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. (https://toolboxjs.com/grid/guides/automated-testing.md) - **Internationalization** — Translating @toolbox-web/grid — screen reader announcements, plugin UI strings, cell value formatting, and right-to-left layout. (https://toolboxjs.com/grid/guides/i18n.md) - **Performance** — Optimize @toolbox-web/grid for large datasets — virtualization tuning, bundle optimization, rendering best practices, and benchmarks. (https://toolboxjs.com/grid/guides/performance.md) - **Theming** — Customize @toolbox-web/grid with CSS custom properties — colors, spacing, typography, dark mode, pre-built themes, and cascade layers. (https://toolboxjs.com/grid/guides/theming.md) - **Touch Input** — How @toolbox-web/grid works on touch devices — gesture map, long-press interactions, hit-target sizing, and known browser quirks. (https://toolboxjs.com/grid/guides/touch-input.md) - **Troubleshooting** — Solutions to common issues when working with @toolbox-web/grid — height and virtualization problems, performance tuning, plugin conflicts, and framework adapter development. (https://toolboxjs.com/grid/guides/troubleshooting.md) ### Plugins - **Clipboard Plugin** — Copy and paste grid data with Excel-compatible clipboard support. (https://toolboxjs.com/grid/plugins/clipboard.md) - **Column Virtualization Plugin** — Improve performance for grids with many columns by only rendering visible columns. (https://toolboxjs.com/grid/plugins/column-virtualization.md) - **Context Menu Plugin** — Add right-click context menus to the grid with customizable items. (https://toolboxjs.com/grid/plugins/context-menu.md) - **Editing Plugin** — Enable inline cell editing with built-in and custom editors. (https://toolboxjs.com/grid/plugins/editing.md) - **Export Plugin** — Export grid data to CSV or other formats. (https://toolboxjs.com/grid/plugins/export.md) - **Filtering Plugin** — Add column-level filtering with built-in filter panel and custom filters. (https://toolboxjs.com/grid/plugins/filtering.md) - **Column Grouping Plugin** — Group columns visually under shared parent headers. (https://toolboxjs.com/grid/plugins/grouping-columns.md) - **Row Grouping Plugin** — Group rows by column values with expandable groups. (https://toolboxjs.com/grid/plugins/grouping-rows.md) - **Master-Detail Plugin** — Show expandable detail rows beneath data rows. (https://toolboxjs.com/grid/plugins/master-detail.md) - **Multi-Sort Plugin** — Sort by multiple columns with shift-click support. (https://toolboxjs.com/grid/plugins/multi-sort.md) - **Pinned Columns Plugin** — Pin columns to the left or right side of the grid. (https://toolboxjs.com/grid/plugins/pinned-columns.md) - **Pinned Rows (Status Bar) Plugin** — Pin summary or custom rows to the top or bottom of the grid. (https://toolboxjs.com/grid/plugins/pinned-rows.md) - **Pivot Table Plugin** — Transform row data into a cross-tabulation (pivot table) layout. (https://toolboxjs.com/grid/plugins/pivot.md) - **Print Plugin** — Print the grid contents with configurable page settings. (https://toolboxjs.com/grid/plugins/print.md) - **Column Reorder Plugin** — Allow users to reorder columns by drag and drop. (https://toolboxjs.com/grid/plugins/reorder-columns.md) - **Responsive Plugin** — Automatically adapt the grid layout for different screen sizes. (https://toolboxjs.com/grid/plugins/responsive.md) - **Row Drag-Drop Plugin** — Drag rows within a single grid (reorder) and across grids that share a drop zone. (https://toolboxjs.com/grid/plugins/row-drag-drop.md) - **Selection Plugin** — Cell, row, and range selection with full keyboard support, conditional selection, and checkbox mode. (https://toolboxjs.com/grid/plugins/selection.md) - **Server-Side Plugin** — Connect the grid to server-side data sources with virtual scrolling. (https://toolboxjs.com/grid/plugins/server-side.md) - **Shell Plugin** — Wrap the grid with a header bar (title + toolbar) and a collapsible tool-panel sidebar. (https://toolboxjs.com/grid/plugins/shell.md) - **Sticky Rows Plugin** — Pin selected data rows below the header as the user scrolls past them. (https://toolboxjs.com/grid/plugins/sticky-rows.md) - **Tooltip Plugin** — Display popover tooltips for truncated header and cell text, with per-column overrides. (https://toolboxjs.com/grid/plugins/tooltip.md) - **Tree Plugin** — Display hierarchical data as an expandable tree. (https://toolboxjs.com/grid/plugins/tree.md) - **Undo/Redo Plugin** — Add undo/redo support for cell edits. (https://toolboxjs.com/grid/plugins/undo-redo.md) - **Column Visibility Plugin** — Allow users to toggle column visibility via a panel. (https://toolboxjs.com/grid/plugins/visibility.md) ### Recipes - **Recipes** — Task-oriented how-to guides for @toolbox-web/grid — editable grids, streaming data, scroll-driven UI, PDF export, spreadsheet formulas, in-cell charts, and input masking. (https://toolboxjs.com/grid/recipes.md) - **Editable grid with undo** — Inline cell editing with undo/redo, dirty tracking, and validation that rejects a bad value before it is committed. (https://toolboxjs.com/grid/recipes/editable-grid-with-undo.md) - **Real-time streaming data** — Push live WebSocket, SSE, or polling updates into the grid with row transactions, without re-assigning the whole dataset. (https://toolboxjs.com/grid/recipes/real-time-streaming.md) - **Scroll & render driven UI** — Infinite scroll, lazy cell hydration, and focusing a freshly added row using the grid's tbw-scroll and render events. (https://toolboxjs.com/grid/recipes/scroll-and-render.md) ### Plugin Development - **Plugin Development** — Build plugins that extend @toolbox-web/grid — lifecycle hooks, manifests, communication, feature registration, and bundling. (https://toolboxjs.com/grid/plugin-development.md) - **Plugin Architecture** — How the @toolbox-web/grid plugin system works internally — lifecycle, hooks, communication, manifests, validated properties, and the feature registry. (https://toolboxjs.com/grid/plugin-development/architecture.md) - **Custom Plugins** — Build custom plugins for @toolbox-web/grid — lifecycle hooks, communication, queries, manifests, testing, and complete examples. (https://toolboxjs.com/grid/plugin-development/custom-plugins.md) ### Framework Adapters - **Framework Adapters** — How React, Angular, and Vue adapters let @toolbox-web/grid render framework-native components as cells, editors, and tool panels. (https://toolboxjs.com/grid/framework-adapters.md) - **Framework Adapter Architecture** — How the @toolbox-web/grid framework-adapter system intercepts grid rendering to mount React, Angular, and Vue components in cells, editors, and tool panels. (https://toolboxjs.com/grid/framework-adapters/architecture.md) ### Angular - **Base Classes for Editors & Filter Panels** — BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, and BaseFilterPanel — reusable base classes for custom Angular editors and filter panels. (https://toolboxjs.com/grid/angular/base-classes.md) - **Angular Integration** — Install and configure @toolbox-web/grid-angular — feature inputs, renderers, editors, events, inject functions, and Angular-specific patterns. (https://toolboxjs.com/grid/angular/getting-started.md) - **Reactive Forms Integration** — Bind @toolbox-web/grid to Angular FormArray — cell-level validation, dirty tracking, lazy form binding, and automatic validation styling. (https://toolboxjs.com/grid/angular/reactive-forms.md) ### React - **React Integration** — Install and configure @toolbox-web/grid-react — feature props, JSX renderers, editors, hooks, and event handling. (https://toolboxjs.com/grid/react/getting-started.md) ### Vue - **Vue Components** — Props, slots, emits and exposed members for every component exported by @toolbox-web/grid-vue. (https://toolboxjs.com/grid/vue/components.md) - **Vue Integration** — Install and configure @toolbox-web/grid-vue — feature props, slot renderers, editors, composables, and event handling. (https://toolboxjs.com/grid/vue/getting-started.md) --- # 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 | Two smaller companions sit alongside them: `llms.txt` (curated link map of every prose page — read this first when you only need to find the right page) and `llms-api.txt` (every generated TypeDoc symbol linked to its `.md` companion — read this when you need an exact type signature). These files are large (the per-framework variants are the smallest — they drop the other stacks' examples and link runnable demos instead of inlining them; the cross-framework `llms-full.txt` is the largest). 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. ## WHERE TO LOOK — task → page Route the user's request to the page that answers it, then fetch that page's `.md` companion (or grep for its `# <title>` heading inside a corpus file). Prefer ONE targeted page over re-reading the whole corpus. Every path below is relative to `https://toolboxjs.com` (e.g. `https://toolboxjs.com/grid/getting-started.md`). | The user wants to… | Read | |---|---| | Install and render a first grid | `/grid/getting-started.md` | | Define columns, formatting, sorting, sizing, row height | `/grid/core.md` | | See every config option / type signature | `/grid/api-reference.md`, then `llms-api.txt` | | Know which plugin solves a need, and compatibility | `/grid/plugins.md` | | Load data from a server, paginate, infinite-scroll | `/grid/plugins/server-side.md` | | Edit cells, validate input, custom editors | `/grid/plugins/editing.md` | | Filter, search, or build a filter panel | `/grid/plugins/filtering.md` | | Select rows/cells/ranges, copy to clipboard | `/grid/plugins/selection.md`, `/grid/plugins/clipboard.md` | | Group, aggregate, pivot, or show a tree | `/grid/plugins/grouping-rows.md`, `/grid/plugins/pivot.md`, `/grid/plugins/tree.md` | | Export to CSV/Excel, or print | `/grid/plugins/export.md`, `/grid/plugins/print.md` | | Restyle, theme, or use dark mode | `/grid/guides/theming.md` | | Fix a blank / zero-height / non-rendering grid | `/grid/guides/troubleshooting.md` | | Understand a `TBW###` console warning or error | `/grid/errors.md` | | Make it fast with large datasets | `/grid/guides/performance.md` | | Wire it up in React / Vue / Angular | `/grid/react/getting-started.md`, `/grid/vue/getting-started.md`, `/grid/angular/getting-started.md` | | Handle SSR, CSP, or browser support | `/grid/platform.md` | | Check supported framework versions, deprecation policy, or release cadence | `/grid/platform.md` | | Translate the grid, or lay it out right-to-left | `/grid/guides/i18n.md` | | Write tests against the grid | `/grid/guides/automated-testing.md` | | Meet accessibility requirements | `/grid/guides/accessibility.md` | | Solve a recurring real-world pattern | `/grid/recipes.md` | | Build a custom plugin or extend the grid | `/grid/plugin-development/custom-plugins.md` | | Upgrade from v2 | `/grid/guides/migration-v3.md` | ## 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<Employee> = { columns: [/* … */], features: { selection: 'range', editing: 'dblclick' }, }; ``` - **React:** `<DataGrid rows={data} gridConfig={gridConfig} />` (memoize with `useMemo`) - **Angular:** `<tbw-grid [rows]="data" [gridConfig]="gridConfig" />` - **Vue:** `<TbwGrid :rows="data" :grid-config="gridConfig" />` (wrap with `markRaw()`) - **Vanilla:** `const grid = createGrid<Employee>(gridConfig); grid.rows = data;` Individual feature props (`<DataGrid selection="row" />`) 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. ## RULE 0.3 — Import from the framework adapter, co-located, framework-scoped Inside a **React / Vue / Angular** project, get import hygiene right — these are the mistakes agents make most: 1. **Import runtime values from the adapter package, never from core.** The component, hooks/composables, providers, directives, and any plugin classes come from `@toolbox-web/grid-{react,vue,angular}`. Do NOT import runtime values (components, `createGrid`, plugin classes) from `@toolbox-web/grid` in framework code. 2. **Import `GridConfig` / `ColumnConfig` from the adapter — NEVER from core.** All three adapters (`@toolbox-web/grid-react`, `@toolbox-web/grid-vue`, `@toolbox-web/grid-angular`) re-export their OWN `GridConfig` / `ColumnConfig` (plus `CellRenderer` / `CellEditor`) that widen `renderer` / `editor` to accept framework components — JSX nodes (React), slots/components (Vue), or component classes (Angular: `{ field: 'x', renderer: MyComponent }`). The base types in `@toolbox-web/grid` type `renderer` as returning `HTMLElement | string` only, so importing them in framework code makes your component renderer/editor fail to type-check. So: `import type { GridConfig, ColumnConfig } from '@toolbox-web/grid-react';` (or `-vue` / `-angular`). Only reach into `@toolbox-web/grid` for types the adapter does not re-export (event-detail types like `CellCommitDetail`, enums, plugin option interfaces). 3. **Never add a bare `import '@toolbox-web/grid';`.** It is redundant in React/Vue/Angular — importing the adapter (`@toolbox-web/grid-{react,vue,angular}`) or any of its `features/*` entries already registers the `<tbw-grid>` custom element as a side effect. A standalone grid-core import is only for vanilla JS/TS with no adapter. 4. **Use the adapter's feature path, not core's.** Inside a framework, side-effect imports must be `@toolbox-web/grid-react/features/selection` (the adapter path), NOT `@toolbox-web/grid/features/selection`. The adapter entry installs the framework-native renderer/editor bridges on top of the core feature — the core path alone can silently drop framework support. 5. **Co-locate feature imports with the grid.** Put `import '@toolbox-web/grid-react/features/…';` in the **same module that renders the grid**, right next to the component that uses it — NOT globally in `main.ts` / `main.tsx` / `App.tsx` / `app.config.ts`. Setup belongs beside its use; a global side-effect import hides which grid needs the feature and defeats per-route code-splitting. WHY: mixing `@toolbox-web/grid` and `@toolbox-web/grid-react` imports (especially the config types), or registering features globally, produces grids that build but silently miss framework-native rendering — or fail to type-check the moment you pass a component as a `renderer` — and makes the dependency graph impossible to reason about. ## 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 JS/TS only (no adapter) import '@toolbox-web/grid-react/features/selection'; // inside a framework — use the adapter path (see RULE 0.3) ``` Put this import in the same module that renders the grid, not in a global entry file. Vanilla JS must also register the element itself: `import '@toolbox-web/grid';` (adapters auto-import it, so never add it in framework code). 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';` (`GridElement` is a legacy alias of the same type). ## 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 **framework-agnostic data grid** web component written in pure TypeScript, with no runtime dependencies. Import it, give it a height, and assign an array of rows. ## 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" <tbw-grid style="height: 300px;"></tbw-grid> ``` #### No build (CDN, single HTML file) Drop one `<script>` tag and you're done — no bundler, no install. Everything is exposed on the global `TbwGrid` object. ```html title="index.html" <!DOCTYPE html> <script src="https://unpkg.com/@toolbox-web/grid/umd/grid.umd.js"></script> <tbw-grid id="my-grid" style="height: 300px;"></tbw-grid> <script> const grid = TbwGrid.queryGrid('#my-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' }, ]; </script> ``` See the [Getting Started guide](https://toolboxjs.com/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 Six patterns do most of the work: | 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](https://toolboxjs.com/grid/architecture.md) for implementation details. ## Plugin System The core grid is small. Everything beyond rendering, sorting and virtualization ships as a tree-shakeable plugin — 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](https://toolboxjs.com/grid/plugins.md) Plugins get dependency validation, type-safe config extension, and auto-cleanup via `AbortSignal`. They use the same public API third parties do, so you can [build and distribute your own](https://toolboxjs.com/grid/plugin-development/custom-plugins.md). ## Next Steps - [Getting Started](https://toolboxjs.com/grid/getting-started.md): Detailed setup for Vanilla JS, React, Vue, and Angular - [Core Features](https://toolboxjs.com/grid/core.md): Sorting, rendering, keyboard navigation, and interactive playground - [Plugins](https://toolboxjs.com/grid/plugins.md): Extend with selection, filtering, grouping, and 24+ more - [API Reference](https://toolboxjs.com/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](https://toolboxjs.com/grid/angular/getting-started.md), [React](https://toolboxjs.com/grid/react/getting-started.md), or [Vue](https://toolboxjs.com/grid/vue/getting-started.md). :::tip[Prefer declarative HTML with almost no JS?] See [Plain JavaScript (No Build Step)](#plain-javascript-no-build-step) below for zero-JS setup with `@toolbox-web/grid/all`, declarative columns, template-based rendering/editing, and UMD script-tag usage. ::: ## 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 <script src="https://unpkg.com/@toolbox-web/grid/umd/grid.umd.js"></script> ``` 2. **Import and use** ```typescript import '@toolbox-web/grid'; // registers <tbw-grid> import { queryGrid } from '@toolbox-web/grid'; // typed DOM helper ``` 3. **Add the grid to your HTML** ```html <tbw-grid id="my-grid" style="height: 400px;"></tbw-grid> ``` :::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 `<tbw-grid>` itself. ::: ### Declarative Columns (No JavaScript) Define columns directly in HTML — great for static layouts and quick prototyping: ```html <tbw-grid style="height: 400px;"> <tbw-grid-column field="id" header="ID" type="number" sortable></tbw-grid-column> <tbw-grid-column field="name" header="Name"></tbw-grid-column> <tbw-grid-column field="email" header="Email"></tbw-grid-column> </tbw-grid> ``` Set `rows` from JavaScript or via the `rows` HTML attribute (JSON). See [Light DOM Columns](https://toolboxjs.com/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](https://toolboxjs.com/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. ::: #### TypeScript Add a grid element to your HTML, then configure it from JavaScript: ```html title="index.html" <tbw-grid style="height: 400px;"></tbw-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' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]; grid.rows = [ { 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' }, ]; ``` That's a working grid. From here, add features as you need them — sorting, editing, selection, and more are each a one-line import: ```typescript title="main.ts (with features)" import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/editing'; import '@toolbox-web/grid/features/selection'; import '@toolbox-web/grid/features/filtering'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, ], features: { editing: 'dblclick', selection: 'row', filtering: true, }, }; grid.rows = [ { 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' }, ]; grid.on('cell-commit', (detail) => console.log('Edited:', detail)); ``` See [Core Features](https://toolboxjs.com/grid/core.md) for renderers, formatters, and custom editors. :::note[Prefer creating grids programmatically?] Use `createGrid()` to create a `<tbw-grid>` element without writing HTML: ```typescript import { createGrid } from '@toolbox-web/grid'; const grid = createGrid({ columns: [...], features: { ... } }); document.body.appendChild(grid); ``` ::: #### React For React projects, use the `@toolbox-web/grid-react` adapter so renderers and editors can be React components: ```bash # Install both packages npm install @toolbox-web/grid @toolbox-web/grid-react ``` ```tsx title="EmployeeGrid.tsx" import { DataGrid } from '@toolbox-web/grid-react'; const 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' }, ]; function EmployeeGrid() { return ( <DataGrid rows={employees} columns={[ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]} style={{ height: 400, display: 'block' }} /> ); } ``` That's a working grid. From here, add features as you need them — each is a one-line import plus a prop: ```tsx title="EmployeeGrid.tsx (with features)" import '@toolbox-web/grid-react/features/editing'; import '@toolbox-web/grid-react/features/selection'; import '@toolbox-web/grid-react/features/filtering'; import { DataGrid } from '@toolbox-web/grid-react'; const 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' }, ]; function EmployeeGrid() { return ( <DataGrid rows={employees} columns={[ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, ]} editing="dblclick" selection="row" filtering onCellCommit={(e) => console.log('Edited:', e.detail)} style={{ height: 400, display: 'block' }} /> ); } ``` The adapter adds JSX renderers/editors, the `useGrid` hook, and declarative `GridColumn` components. See the [React adapter docs](https://toolboxjs.com/grid/react/getting-started.md) for custom renderers, editors, and the complete API reference. #### Vue For Vue projects, use the `@toolbox-web/grid-vue` adapter so renderers and editors can be Vue components or slots: ```bash # Install both packages npm install @toolbox-web/grid @toolbox-web/grid-vue ``` ```html title="EmployeeGrid.vue" <script setup> import { TbwGrid } from '@toolbox-web/grid-vue'; const 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' }, ]; </script> <template> <TbwGrid :rows="employees" :columns="[ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]" style="height: 400px; display: block;" /> </template> ``` That's a working grid. From here, add features as you need them — each is a one-line import plus a prop: ```html title="EmployeeGrid.vue (with features)" <script setup> import '@toolbox-web/grid-vue/features/editing'; import '@toolbox-web/grid-vue/features/selection'; import '@toolbox-web/grid-vue/features/filtering'; import { TbwGrid } from '@toolbox-web/grid-vue'; const 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' }, ]; </script> <template> <TbwGrid :rows="employees" :columns="[ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, ]" editing="dblclick" selection="row" filtering @cell-commit="(e) => console.log('Edited:', e.detail)" style="height: 400px; display: block;" /> </template> ``` The adapter adds slot-based renderers/editors (`#cell`, `#editor`), the `useGrid` composable, and declarative `TbwGridColumn` components. See the [Vue adapter docs](https://toolboxjs.com/grid/vue/getting-started.md) for custom renderers, editors, and the complete API reference. #### Angular For Angular projects, use the `@toolbox-web/grid-angular` adapter so renderers and editors can be Angular templates or components: ```bash # Install both packages npm install @toolbox-web/grid @toolbox-web/grid-angular ``` ```typescript title="grid.component.ts" import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { ColumnConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <tbw-grid [rows]="employees" [columns]="columns" style="height: 400px; display: block;"> </tbw-grid> `, }) 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 { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { ColumnConfig } from '@toolbox-web/grid-angular'; import type { CellCommitDetail } from '@toolbox-web/grid/plugins/editing'; @Component({ selector: 'app-employee-grid', imports: [Grid, GridEditingDirective, GridSelectionDirective, GridFilteringDirective], template: ` <tbw-grid [rows]="employees" [columns]="columns" [editing]="'dblclick'" [selection]="'row'" [filtering]="true" (cellCommit)="onCellCommit($event)" style="height: 400px; display: block;"> </tbw-grid> `, }) 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](https://toolboxjs.com/grid/angular/getting-started.md) for custom renderers, editors, and the complete API reference. ## Plain JavaScript (No Build Step) This section covers both no-build paths: - ES module `/all` for declarative HTML-first setup - UMD script tags with global `TbwGrid` for classic browser scripting ### HTML-first with `/all` (zero handwritten setup code) Use this approach when you want a fully functional grid from plain HTML. The `/all` bundle auto-registers the grid plus all features/plugins, so you can configure everything with attributes and child elements. :::caution[/all is convenience-first] `@toolbox-web/grid/all` includes every plugin and feature. It is ideal for quick setup and HTML-first pages, but JS applications that care about bundle size should import only the features/plugins they use. ::: #### Zero-JS Quickstart 1. Add the module script: ```html <script type="module" src="https://unpkg.com/@toolbox-web/grid/all.js"></script> ``` 2. Add a grid with JSON attributes: ```html <tbw-grid style="height: 320px;" rows='[{"id":1,"name":"Alice","status":"active"},{"id":2,"name":"Bob","status":"inactive"}]' grid-config='{"features":{"selection":"row","editing":"dblclick","filtering":true,"export":{"fileName":"employees","includeHeaders":true}}}' > <tbw-grid-column field="id" header="ID" type="number" sortable></tbw-grid-column> <tbw-grid-column field="name" header="Name" sortable></tbw-grid-column> <tbw-grid-column field="status" header="Status" editable></tbw-grid-column> </tbw-grid> ``` 3. Interact with the grid: - Sort by clicking any sortable header. - Filter from header filter controls. - Select rows. - Edit `editable` cells (double-click in this example). #### Declarative columns Define columns with `<tbw-grid-column>` children. `field` is the only required attribute, and it accepts the `field:type` shorthand — `field="price:number"` expands to `field="price"`, `type="number"` and a generated header of "Price". ```html <tbw-grid> <tbw-grid-column field="name" header="Name" width="200" sortable></tbw-grid-column> <tbw-grid-column field="salary:number" editable></tbw-grid-column> </tbw-grid> ``` Boolean attributes (`sortable`, `resizable`, `editable`, `hidden`, `lock-visible`) are enabled by their presence. Some are contributed by plugins and only take effect when that plugin is registered. For the complete attribute list, and for related declarative elements such as `<tbw-grid-column-header>` and the shell/tool-panel elements, see [API reference: Light DOM elements](https://toolboxjs.com/grid/api-reference.md#light-dom-elements). #### Template rendering Use `<tbw-grid-column-view>` for display and `<tbw-grid-column-editor>` for custom editors. Supported template expressions: - `{{ value }}` for the current cell value. - `{{ row.field }}` for other fields on the current row. - Ternaries and simple expressions, for example `{{ row.status === 'active' ? 'ok' : 'bad' }}`. Security boundaries: - Script execution and event-handler attributes are not allowed in template output. - HTML-string rendering is sanitized. - Keep behavior wiring out of templates; these templates are for safe markup/data interpolation. ##### Live Demo ```ts // HtmlOnlyUsageDemo.astro import '@toolbox-web/grid/all'; ``` This demo uses only declarative HTML and one `/all` script import. It includes a conditional badge class and a `<select>` editor, both expressed entirely in HTML templates. ### UMD script-tag usage (global `TbwGrid`) If you prefer classic script tags and a global API, use the UMD bundles and configure the grid through `window.TbwGrid`. #### Core UMD bundle ```html title="index.html" <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/@toolbox-web/grid/umd/grid.umd.js"></script> </head> <body> <tbw-grid id="my-grid" style="height: 400px;"></tbw-grid> <script> var grid = TbwGrid.queryGrid('#my-grid'); grid.columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]; grid.rows = [ { 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' }, ]; </script> </body> </html> ``` #### All-in-one UMD bundle (core + plugins) ```html title="index.html (with plugins)" <script src="https://unpkg.com/@toolbox-web/grid/umd/grid.all.umd.js"></script> <script> var grid = TbwGrid.queryGrid('#my-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, ], plugins: [ new TbwGrid.EditingPlugin({ trigger: 'dblclick' }), new TbwGrid.SelectionPlugin({ mode: 'row' }), new TbwGrid.FilteringPlugin(), ], }; grid.rows = [ { 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' }, ]; </script> ``` ### Bundle size notes `/all` is the easiest entry point for HTML-first usage and quick prototyping. For application builds where bundle size matters, prefer targeted imports (`@toolbox-web/grid` plus specific `features/*` and `plugins/*`) so your bundler can tree-shake unused modules. For script-tag usage, `grid.umd.js` includes core only (~168 kB raw, ~48 kB gzipped), while `grid.all.umd.js` bundles core + all plugins (~514 kB raw, ~136 kB gzipped). You can also load individual plugin UMD files (for example `selection.umd.js`) for finer control. ## 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<Employee>('tbw-grid'); // Column config is type-checked against Employee const columns: ColumnConfig<Employee>[] = [ { 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 }); ``` ### Nested fields A column `field` can be a **dotted path** into nested row data — it is read and written without a `valueAccessor`: ```typescript interface Deal { id: string; deal: { capture: { field: string; comments: string[] }; otherStuff: { other: string }; }; } const columns: ColumnConfig<Deal>[] = [ { field: 'deal.capture.field' }, { field: 'deal.capture.comments', renderer: (ctx) => (ctx.value as string[]).join(', ') }, { field: 'deal.otherStuff.other' }, ]; ``` Dotted paths work everywhere a plain field does — display, sort, filter, export, editing, and undo/redo. Rules worth knowing: - If a column defines a `valueAccessor`, it still wins over the dotted-path read. - If a row literally has an own property named `'deal.comments'` (a flat key containing a dot), that flat value is used instead of traversing. - By default `field` accepts **any** string (dotted paths compile with zero config) while still autocompleting top-level keys. For strict compile-time validation of the full path, opt in with the `NestedPaths` helper: ```typescript import type { GridConfig, NestedPaths } from '@toolbox-web/grid'; // Typos anywhere in the path are now compile errors. const config: GridConfig<Deal, NestedPaths<Deal>> = { columns: [{ field: 'deal.capture.field' }], }; ``` ## Next Steps Now that you have the grid set up, explore: - [Core Features](https://toolboxjs.com/grid/core.md): Sorting, editing, keyboard navigation, and interactive playground - [Selection Plugin](https://toolboxjs.com/grid/plugins/selection.md): Add row, cell, or range selection - [Theming](https://toolboxjs.com/grid/guides/theming.md): Customize colors, spacing, and typography - [API Reference](https://toolboxjs.com/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<ColumnKey, ColumnConfig> = { 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<string, unknown>[] = []; 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 | |-----|--------| | <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> | Move between cells | | <kbd>Home</kbd> / <kbd>End</kbd> | Jump to first/last cell in row | | <kbd>Ctrl</kbd> + <kbd>Home</kbd> / <kbd>Ctrl</kbd> + <kbd>End</kbd> | Jump to first/last cell in grid | | <kbd>PgUp</kbd> / <kbd>PgDn</kbd> | Scroll by viewport height | | <kbd>↵ Enter</kbd> | Start editing (with EditingPlugin) | | <kbd>Esc</kbd> | Cancel editing | | <kbd>⇥ Tab</kbd> / <kbd>⇧ Shift</kbd> + <kbd>⇥ Tab</kbd> | Move to next/previous editable cell | For the full keyboard shortcut reference, see the [Accessibility guide](https://toolboxjs.com/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 <tbw-grid dir="rtl"></tbw-grid> ``` ```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](https://toolboxjs.com/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 `<tbw-grid-column>` elements (or framework wrapper components) to declaratively define columns directly in your markup: #### TypeScript ```html <tbw-grid> <tbw-grid-column field="id" header="ID" width="80"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" sortable></tbw-grid-column> <tbw-grid-column field="email" header="Email Address"></tbw-grid-column> </tbw-grid> ``` #### React ```tsx import { DataGrid, GridColumn } from '@toolbox-web/grid-react'; function EmployeeGrid({ rows }) { return ( <DataGrid rows={rows}> <GridColumn field="id" header="ID" width={80} /> <GridColumn field="name" header="Full Name" sortable /> <GridColumn field="email" header="Email Address" /> </DataGrid> ); } ``` #### Vue ```html <script setup lang="ts"> import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue'; </script> <template> <TbwGrid :rows="rows"> <TbwGridColumn field="id" header="ID" :width="80" /> <TbwGridColumn field="name" header="Full Name" sortable /> <TbwGridColumn field="email" header="Email Address" /> </TbwGrid> </template> ``` #### Angular ```html <tbw-grid [rows]="rows"> <tbw-grid-column field="id" header="ID" width="80"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" sortable></tbw-grid-column> <tbw-grid-column field="email" header="Email Address"></tbw-grid-column> </tbw-grid> ``` 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: #### TypeScript ```html <tbw-grid> <tbw-grid-column field="id" header="ID" order="2"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" order="0"></tbw-grid-column> <tbw-grid-column field="email" header="Email Address" order="1"></tbw-grid-column> </tbw-grid> ``` The columns will appear in order: `name`, `email`, `id`. #### React ```tsx <DataGrid rows={rows}> <GridColumn field="id" header="ID" order={2} /> <GridColumn field="name" header="Full Name" order={0} /> <GridColumn field="email" header="Email Address" order={1} /> </DataGrid> ``` #### Vue ```html <TbwGrid :rows="rows"> <TbwGridColumn field="id" header="ID" :order="2" /> <TbwGridColumn field="name" header="Full Name" :order="0" /> <TbwGridColumn field="email" header="Email Address" :order="1" /> </TbwGrid> ``` #### Angular ```html <tbw-grid [rows]="rows"> <tbw-grid-column field="id" header="ID" order="2"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" order="0"></tbw-grid-column> <tbw-grid-column field="email" header="Email Address" order="1"></tbw-grid-column> </tbw-grid> ``` 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](https://toolboxjs.com/grid/plugins/reorder-columns.md)) take precedence after that. Use [`resetColumnOrder()`](https://toolboxjs.com/grid/plugins/reorder-columns/classes/reorderplugin.md#resetcolumnorder) to restore the order-attribute positioning. **Combining with `columnInference: 'merge'`** — The `order` attribute is most useful in `merge` mode. With inference enabled, the grid automatically displays all data fields, and you can use light-DOM `<tbw-grid-column>` 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). [`GridConfig`](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md) is the full, type-checked reference — every option, its exact type and its default live there. The options below are the ones you will reach for first: | Property | Type | Description | |----------|------|-------------| | `columns` | [`ColumnConfig[]`](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md) | Column definitions | | `rows` | `any[]` | Row data array (top-level grid prop, not on `GridConfig`) | | `getRowId` | `(row) => string` | Unique row identity function | | `fitMode` | [`FitMode`](https://toolboxjs.com/grid/api/core/types/fitmode.md) | How columns fill available width (`'stretch'` or `'fixed'`) | | `columnInference` | [`ColumnInferenceMode`](https://toolboxjs.com/grid/api/core/types/columninferencemode.md) | How inference combines with provided columns (`'auto'` default, or `'merge'`) | | `rowHeight` | `number \| (row, index) => number \| undefined` | Fixed or variable row heights | | `typeDefaults` | <code>Record<string, <a href="/grid/api/core/interfaces/typedefault/">TypeDefault</a>></code> | Default column config per column `type` | | `plugins` | [`GridPlugin[]`](https://toolboxjs.com/grid/api/plugin-development/interfaces/gridplugin.md) | Plugin instances | | `features` | [`Partial<FeatureConfig>`](https://toolboxjs.com/grid/api/core/interfaces/featureconfig.md) | Declarative feature config (alternative to `plugins`) | The remainder of `GridConfig` covers grid-wide toggles (`sortable`, `resizable`), initial state (`initialSort`, `columnState`), presentation overrides (`icons`, `animation`, `rowClass`, `loadingRenderer`, `emptyRenderer`, `emptyOverlay`), accessibility (`a11y`, `gridAriaLabel`, `gridAriaDescribedBy`) and the low-level `sortHandler` escape hatch. **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 **A `valueAccessor` is the single source of truth for a column's value.** Think of it as answering one question for the grid — _"what is this cell's value?"_ — as a function of the **whole row** instead of a single property. By default the grid reads `row[field]`. `field` may be a **dotted path** into a nested structure like `deal.capture.instrument`, resolved with no extra config. When you supply a `valueAccessor`, **every feature that needs a cell's value reads through it** — not just the renderer. That covers filtering, sorting, aggregation (`sum` / `avg` / …, including group-row totals), copy-to-clipboard, CSV/Excel export, and the default display. You write the value logic once and the whole grid stays consistent. ```ts // NestedFieldsDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; interface Deal { id: number; deal: { capture: { instrument: string; comments: string[] }; counterparty: { name: string }; economics: { qty: number; price: number }; }; } const rows: Deal[] = [ { id: 1, deal: { capture: { instrument: 'Brent Crude', comments: ['priced', 'confirmed'] }, counterparty: { name: 'Acme Trading' }, economics: { qty: 1000, price: 82.5 }, }, }, { id: 2, deal: { capture: { instrument: 'WTI', comments: ['pending'] }, counterparty: { name: 'Globex Energy' }, economics: { qty: 2500, price: 78.1 }, }, }, { id: 3, deal: { capture: { instrument: 'Nat Gas', comments: [] }, counterparty: { name: 'Initech Power' }, economics: { qty: 500, price: 3.42 }, }, }, ]; const grid = queryGrid<Deal>('#demo-nested-fields'); if (grid) { grid.gridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, // Dotted paths — read straight from the nested structure, no accessor. { field: 'deal.capture.instrument', header: 'Instrument' }, { field: 'deal.counterparty.name', header: 'Counterparty' }, // Array leaf — a dotted path plus a renderer to present the array. { field: 'deal.capture.comments', header: 'Comments', renderer: (ctx) => (ctx.value as string[]).join(', ') || '—', }, // Computed value — this is what `valueAccessor` is for. { field: 'total', header: 'Total', valueAccessor: ({ row }) => row.deal.economics.qty * row.deal.economics.price, format: (v) => `$${Number(v).toLocaleString()}`, }, ], }; grid.rows = rows; } ``` **When do you actually need one?** Two situations: - **The value isn't naturally in the dataset** — it has to be _computed_ or _derived_: multiplied from sibling fields (`qty * price`), looked up inside a nested array, joined from several properties (`firstName + lastName`), or pulled from somewhere outside the row entirely. - **You want to transform the stored value** before any feature sees it — normalize, coerce, round, or map a raw code to a human label — so that filtering, sorting, and export all operate on the cleaned-up value rather than the raw one. You _could_ fake the _visual_ result with a `renderer` or `format` function alone — but those only change how the cell **looks**. Everything that reasons about the cell's **value** (filter matches, sort order, group keys, aggregates, clipboard, exports) would still see `undefined` or the raw `row[field]`. A `valueAccessor` fixes the value at the source so every consumer agrees; `renderer` / `format` then layer presentation on top. ```typescript grid.columns = [ // Plain nested read — just use a dotted `field`, no accessor needed. { field: 'deal.capture.instrument', header: 'Instrument' }, // Computed from sibling fields { field: 'total', header: 'Total', valueAccessor: ({ row }) => row.qty * row.price, }, // Pluck from a nested array { field: 'lastShipmentDate', header: '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]` | | **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. > **Row grouping** is the exception: group **keys** come from the `groupOn` callback you supply, not from `valueAccessor`. Only the aggregated _values_ shown on group rows go through the accessor. #### 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(); }); } ``` #### Vue ```typescript import { ref, watch } from 'vue'; import { invalidateAccessorCache } from '@toolbox-web/grid'; const fxRate = ref(1.0); const columns = [{ field: 'totalUsd', valueAccessor: ({ row }) => row.totalEur * fxRate.value, }]; watch(fxRate, () => { invalidateAccessorCache(); gridRef.value?.requestRender(); }); ``` #### React ```typescript import { useEffect } from 'react'; import { invalidateAccessorCache } from '@toolbox-web/grid'; const [fxRate, setFxRate] = useState(1.0); const columns = useMemo(() => [{ field: 'totalUsd', valueAccessor: ({ row }) => row.totalEur * fxRate, }], [fxRate]); useEffect(() => { invalidateAccessorCache(); gridRef.current?.requestRender(); }, [fxRate]); ``` ::: #### 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`. :::tip[Need spreadsheet-style expressions?] The grid has no formula engine, but the accessor is the seam for one — [Recipes: Spreadsheet formulas](https://toolboxjs.com/grid/recipes/spreadsheet-formulas.md) wires HyperFormula through `valueAccessor`. ::: --- ### 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 /* Dim with a surface tint rather than `opacity` — `opacity` fades text as well as chrome and quickly drops it below the 4.5:1 WCAG 1.4.3 minimum. */ .row-inactive { background: light-dark(#f1f2f4, #26282d); font-style: italic; } ``` :::note `rowClass` is applied to custom-rendered rows too — a Responsive `cardRenderer` card gets the same classes as the table row it replaces. It is only skipped for rows a plugin both synthesizes **and** renders itself (group headers, pivot rows, grouped loading placeholders), since those are not your data. Server-side `{ __loading: true }` placeholder rows are *not* skipped, so guard on `__loading` if your callback reads fields off the row. The callback runs during rendering, so a class that depends on state outside the row object (a selected id, for example) only updates on the next render — call `grid.requestRender()` when that state changes. ::: ### 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<Employee>('#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 #### TypeScript ```typescript grid.columns = [ { field: 'status', header: 'Status', renderer: (ctx) => { const badge = document.createElement('span'); badge.className = `badge badge-${ctx.value}`; badge.textContent = ctx.value; return badge; }, }, { field: 'active', header: 'Active', renderer: (ctx) => { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !!ctx.value; checkbox.disabled = true; // A disabled control still needs an accessible name. checkbox.setAttribute('aria-label', 'Active'); return checkbox; }, }, ]; ``` #### React ```tsx import { DataGrid, GridColumn } from '@toolbox-web/grid-react'; function EmployeeGrid({ rows }) { return ( <DataGrid rows={rows}> <GridColumn field="status" header="Status" renderer={({ value }) => ( <span className={`badge badge-${value}`}>{value}</span> )} /> <GridColumn field="active" header="Active" renderer={({ value }) => ( <input type="checkbox" checked={!!value} disabled aria-label="Active" /> )} /> </DataGrid> ); } ``` #### Vue ```html <script setup lang="ts"> import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue'; </script> <template> <TbwGrid :rows="rows"> <TbwGridColumn field="status" header="Status"> <template #cell="{ value }"> <span :class="`badge badge-${value}`">{{ value }}</span> </template> </TbwGridColumn> <TbwGridColumn field="active" header="Active"> <template #cell="{ value }"> <input type="checkbox" :checked="!!value" disabled aria-label="Active" /> </template> </TbwGridColumn> </TbwGrid> </template> ``` #### Angular ```typescript import { Component } from '@angular/core'; import { Grid, TbwRenderer } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid, TbwRenderer], template: ` <tbw-grid [rows]="rows" style="height: 400px; display: block"> <tbw-grid-column field="status" header="Status"> <span *tbwRenderer="let value" [class]="'badge badge-' + value"> {{ value }} </span> </tbw-grid-column> <tbw-grid-column field="active" header="Active"> <input *tbwRenderer="let value" type="checkbox" [checked]="!!value" disabled aria-label="Active" /> </tbw-grid-column> </tbw-grid> `, }) export class EmployeeGridComponent { rows = [/* ... */]; } ``` The renderer receives a [`CellRenderContext`](https://toolboxjs.com/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<Employee>('#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 }) => { // Solid pill rather than a 13%-alpha tint: the tinted variant left the // label at 2.8:1 in light mode and 4.1:1 in dark (WCAG 1.4.3 needs 4.5). // These shades all clear 5:1 against white text in either scheme. const colors: Record<string, string> = { active: '#15803d', inactive: '#b91c1c', 'on-leave': '#b45309', }; 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] ?? '#4b5563'}; color: #ffffff; 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; // A control still needs an accessible name even when it is disabled; // without it the cell is announced as a bare "checkbox". checkbox.setAttribute('aria-label', 'Active'); 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 }, ]; } ``` :::tip[Charts in cells?] A renderer returns a DOM node, so an inline `<svg>` is enough for a trend column — see [Recipes: In-cell sparklines](https://toolboxjs.com/grid/recipes/sparklines.md). ::: #### Light DOM renderers You can also define a cell renderer **declaratively in your template** — favouring markup over an imperative JavaScript callback. It is not the highest-performance path, but it has its place when you want more configuration in the template and less in JavaScript (a step toward setting up a grid with as little JS as possible). Every stack supports it: a light-DOM `<tbw-grid-column-view>` element in vanilla (use `{{ value }}` to interpolate the cell value, and `{{ row.field }}` for other row fields), a `renderer` prop in React, a `#cell` slot in Vue, and the `*tbwRenderer` directive in Angular: #### TypeScript ```html <tbw-grid column-inference="merge"> <tbw-grid-column field="status"> <tbw-grid-column-view> <span class="badge badge-{{ value }}">{{ value }}</span> </tbw-grid-column-view> </tbw-grid-column> </tbw-grid> ``` #### React ```tsx import { DataGrid, GridColumn } from '@toolbox-web/grid-react'; function StatusGrid({ rows }) { return ( <DataGrid rows={rows}> <GridColumn field="status" renderer={({ value }) => <span className={`badge badge-${value}`}>{value}</span>} /> </DataGrid> ); } ``` #### Vue ```html <script setup lang="ts"> import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue'; </script> <template> <TbwGrid :rows="rows"> <TbwGridColumn field="status"> <template #cell="{ value }"> <span :class="`badge badge-${value}`">{{ value }}</span> </template> </TbwGridColumn> </TbwGrid> </template> ``` #### Angular ```html <tbw-grid [rows]="rows"> <tbw-grid-column field="status"> <span *tbwRenderer="let value" [class]="'badge badge-' + value">{{ value }}</span> </tbw-grid-column> </tbw-grid> ``` The companion light-DOM templates are `<tbw-grid-column-editor>` (custom editor, requires the [Editing plugin](https://toolboxjs.com/grid/plugins/editing.md)) and `<tbw-grid-column-header>` (custom header cell). See the [API reference](https://toolboxjs.com/grid/api-reference.md#column-elements) for the full list. #### 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 <script>, it executes return div; }; ``` If you genuinely need to inject HTML (e.g. rendering a server-trusted markdown snippet), sanitize first with a library like [DOMPurify](https://github.com/cure53/DOMPurify) before assigning to `innerHTML`. :::note[String-returning renderers are auto-sanitized] When a cell renderer or group header renderer returns an **HTML string** (rather than an `HTMLElement` / `Node` / framework component), the grid runs the string through an internal sanitizer that strips `<script>` elements and event-handler attributes (`onclick`, `onerror`, etc.) before inserting it into the DOM. This is defense-in-depth — always validate your data upstream, but accidental injection through these renderer paths will not execute. Renderers that return `HTMLElement` / `Node` / framework components are inserted as-is and are not sanitized; the `textContent` rule above is your responsibility. ::: ### Choosing: Formatter vs Renderer Now that you've seen both, here's a quick reference for picking the right one: | Aspect | Formatter (`format`) | Renderer (`renderer`) | |---|---|---| | **Returns** | String | DOM element | | **Use for** | Currency, dates, percentages, text formatting | Checkboxes, badges, buttons, links, images | | **Performance** | Faster (text only) | Slower (DOM creation) | | **Interactivity** | None (display only) | Full (event listeners, components) | **Rule of thumb:** If you only need to change how a value *looks*, use a `format` function. If you need interactive elements or custom HTML structure, use a `renderer`. ### Row Animation The grid provides a built-in row animation API for highlighting changes, insertions, and removals with visual feedback. | Method | Description | CSS Variable | |--------|-------------|--------------| | `animateRow(i, 'change')` | Flash highlight for data changes | `--tbw-row-change-duration` (500ms) | | `insertRow(i, row)` | Slide-in animation for new rows | `--tbw-row-insert-duration` (300ms) | | `removeRow(i)` | Fade-out animation for removed rows | `--tbw-row-remove-duration` (200ms) | | `applyTransaction(tx)` | Animates add/update/remove in one pass | All of the above | ```typescript // Highlight a row after updating grid.rows[5].status = 'updated'; grid.animateRow(5, 'change'); // Animate by row ID (stable when sorted/filtered) grid.animateRowById(data.id, 'change'); // Animate multiple rows at once grid.animateRows([0, 2, 5], 'change'); ``` ### Row Transactions `applyTransaction()` batches add, update, and remove operations into a **single render cycle** instead of re-assigning the whole dataset. `applyTransactionAsync()` additionally merges every call made within one animation frame. ```typescript interface RowTransaction<T> { add?: T[]; // Appended to the end update?: { id: string; changes: Partial<T> }[]; // In-place mutation by row ID remove?: { id: string }[]; // Removed by row ID } ``` Operations apply in the order **removes → updates → adds**, so updates never target a row that is about to be removed and new rows never collide with existing IDs. Matching by ID requires a `getRowId` function on the grid config. | Scenario | Method | Animations | | --- | --- | --- | | User action, moderate stream (< 10 msg/s) | `applyTransaction()` | Yes (configurable) | | High-frequency ticker (100+ msg/s) | `applyTransactionAsync()` | Disabled (batched) | Both return a `TransactionResult` carrying the actual `added`, `updated`, and `removed` row objects. :::tip[Wiring up a live feed?] [Recipes: Real-time streaming data](https://toolboxjs.com/grid/recipes/real-time-streaming.md) shows the WebSocket and SSE plumbing end to end. ::: **Customize appearance:** ```css tbw-grid { --tbw-row-change-duration: 750ms; --tbw-row-change-color: rgba(34, 197, 94, 0.25); } ``` ```ts // RowAnimationDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; interface Item { id: number; name: string; status: string; } const grid = queryGrid<Item>('#demo-row-animation'); if (grid) { const container = document.querySelector('.grid-demo'); let nextId = 6; const data: Item[] = [ { id: 1, name: 'Alpha', status: 'active' }, { id: 2, name: 'Bravo', status: 'active' }, { id: 3, name: 'Charlie', status: 'pending' }, { id: 4, name: 'Delta', status: 'active' }, { id: 5, name: 'Echo', status: 'inactive' }, ]; grid.columns = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'status', header: 'Status', width: 120 }, ]; grid.rows = data; container.addEventListener('click', (e) => { const action = (e.target as HTMLElement).dataset.animAction; if (!action) return; if (action === 'change') { const idx = Math.floor(Math.random() * grid.rows.length); grid.animateRow(idx, 'change'); } else if (action === 'insert') { const id = nextId++; const row = { id, name: `New-${id}`, status: 'new' }; grid.insertRow(grid.rows.length, row); } else if (action === 'remove' && grid.rows.length > 1) { grid.removeRow(grid.rows.length - 1); } }); } ``` :::note When using the [**EditingPlugin**](https://toolboxjs.com/grid/plugins/editing.md), rows are automatically animated with `'change'` after a successful edit commit. ::: ### Type-Level Defaults Define defaults for all columns of a specific `type`. A type default accepts **any** [`ColumnConfig`](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md) property — `format`, `renderer`, `width`, `sortable`, `cellClass`, `sortComparator`, and so on — plus the column properties contributed by whichever plugins you import (for example `editor` and `editorParams` from the [EditingPlugin](https://toolboxjs.com/grid/plugins/editing.md), or `filterType` from the [FilteringPlugin](https://toolboxjs.com/grid/plugins/filtering.md)). Six properties are **never** inherited from a type default, because they identify or position a single column rather than describe its type: `field`, `header`, `order`, `group`, `hidden` and `utility`. Resolution is per-property and shallow: a column keeps any value it defines itself (including falsy ones such as `sortable: false` or `width: 0`), and inherits the type-level value only where it left the property `undefined` or `null`. #### TypeScript ```typescript grid.gridConfig = { typeDefaults: { currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(value), }, boolean: { renderer: (ctx) => { const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!ctx.value; cb.disabled = true; return cb; }, }, status: { renderer: (ctx) => { const badge = document.createElement('span'); badge.className = `status-badge status-${ctx.value}`; badge.textContent = ctx.value; return badge; }, }, // Any column property works, not just format/renderer: percent: { width: 110, cellClass: 'numeric', sortComparator: (a, b) => (a as number) - (b as number), }, }, columns: [ { field: 'salary', type: 'currency' }, { field: 'active', type: 'boolean' }, { field: 'status', type: 'status' }, { field: 'margin', type: 'percent' }, // Column-level values always win — this one stays 220px wide. { field: 'growth', type: 'percent', width: 220 }, ], }; ``` #### React ```tsx import { GridTypeProvider, DataGrid, GridColumn } from '@toolbox-web/grid-react'; import type { TypeDefaultsMap } from '@toolbox-web/grid-react'; const typeDefaults: TypeDefaultsMap = { currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value), }, boolean: { renderer: ({ value }) => <input type="checkbox" checked={!!value} disabled aria-label="Active" />, }, status: { renderer: ({ value }) => ( <span className={`status-badge status-${value}`}>{value}</span> ), }, }; function App() { return ( <GridTypeProvider defaults={typeDefaults}> <DataGrid rows={rows}> <GridColumn field="salary" type="currency" /> <GridColumn field="active" type="boolean" /> <GridColumn field="status" type="status" /> </DataGrid> </GridTypeProvider> ); } ``` #### Vue ```html <script setup lang="ts"> import { h } from 'vue'; import { GridTypeProvider, TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue'; import CheckboxCell from './CheckboxCell.vue'; import StatusBadge from './StatusBadge.vue'; const typeDefaults = { currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value), }, boolean: { renderer: ({ value }) => h(CheckboxCell, { checked: !!value }), }, status: { renderer: ({ value }) => h(StatusBadge, { status: value }), }, }; </script> <template> <GridTypeProvider :defaults="typeDefaults"> <TbwGrid :rows="rows"> <TbwGridColumn field="salary" type="currency" /> <TbwGridColumn field="active" type="boolean" /> <TbwGridColumn field="status" type="status" /> </TbwGrid> </GridTypeProvider> </template> ``` #### Angular ```typescript import { Component } from '@angular/core'; import { Grid, provideGridTypeDefaults } from '@toolbox-web/grid-angular'; import { CheckboxCellComponent } from './checkbox-cell.component'; import { StatusBadgeComponent } from './status-badge.component'; @Component({ imports: [Grid], providers: [ provideGridTypeDefaults({ currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value), }, boolean: { renderer: CheckboxCellComponent }, status: { renderer: StatusBadgeComponent }, }), ], template: ` <tbw-grid [rows]="rows" [columns]="columns" /> `, }) export class AppComponent { columns = [ { field: 'salary', type: 'currency' }, { field: 'active', type: 'boolean' }, { field: 'status', type: 'status' }, ]; } ``` Type defaults reduce repetition when many columns share the same presentation. Column-level `format` or `renderer` overrides type defaults when both are specified. #### Declarative Light DOM Type Defaults You can also define type defaults directly in light DOM with `<tbw-grid-type>`. ```ts // TypeDefaultsLightDomDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/editing'; const grid = queryGrid('#demo-type-defaults-light-dom'); if (grid) { grid.gridConfig = { features: { editing: { editOn: 'click' } }, // Programmatic typeDefaults win over declarative tbw-grid-type when both exist. // typeDefaults: { currency: { renderer: ... } } }; grid.rows = [ { amount: 1200, tax: 110 }, { amount: 980, tax: 95 }, { amount: 1575, tax: 142 }, { amount: 730, tax: 68 }, ]; } ``` #### TypeScript ```html <tbw-grid id="orders-grid"> <tbw-grid-type name="currency" data-currency="USD" data-locale="en-US"> <tbw-grid-column-view> <span class="money-cell">{{ typeDefault.currency }} {{ value }}</span> </tbw-grid-column-view> <tbw-grid-column-editor> <input type="number" value="{{ value }}" /> </tbw-grid-column-editor> </tbw-grid-type> <tbw-grid-column field="amount" type="currency" editable></tbw-grid-column> <tbw-grid-column field="tax" type="currency" editable></tbw-grid-column> </tbw-grid> <script type="module"> import '@toolbox-web/grid/features/editing'; const grid = document.getElementById('orders-grid'); grid.gridConfig = { features: { editing: { editOn: 'click' } }, // Programmatic typeDefaults win over declarative tbw-grid-type when both exist. // typeDefaults: { currency: { renderer: ... } } }; </script> ``` #### React ```tsx import { DataGrid, GridColumn, GridType } from '@toolbox-web/grid-react'; <DataGrid rows={rows}> <GridType name="currency" params={{ currency: 'USD', locale: 'en-US' }} > {({ value }) => <span className="money-cell">USD {value}</span>} </GridType> <GridColumn field="amount" type="currency" /> <GridColumn field="tax" type="currency" /> </DataGrid> ``` #### Vue ```html <script setup lang="ts"> import { TbwGrid, TbwGridColumn, TbwGridType } from '@toolbox-web/grid-vue'; </script> <template> <TbwGrid :rows="rows"> <TbwGridType name="currency" :params="{ currency: 'USD', locale: 'en-US' }"> <template #cell="{ value }"> <span class="money-cell">USD {{ value }}</span> </template> </TbwGridType> <TbwGridColumn field="amount" type="currency" /> <TbwGridColumn field="tax" type="currency" /> </TbwGrid> </template> ``` #### Angular ```html <tbw-grid [rows]="rows" [editing]="'click'"> <tbw-grid-type name="currency"> <span *tbwRenderer="let value" class="money-cell">USD {{ value }}</span> <input *tbwEditor="let value; onCommit as onCommit" [value]="value" (blur)="onCommit($any($event.target).value)" /> </tbw-grid-type> <tbw-grid-column field="amount" type="currency" editable></tbw-grid-column> <tbw-grid-column field="tax" type="currency" editable></tbw-grid-column> </tbw-grid> ``` Notes: - `<tbw-grid-type>` applies defaults to every column whose `type` matches `name`. - `data-*` attributes on `<tbw-grid-type>` are exposed in templates as `typeDefault.*`. - Programmatic `gridConfig.typeDefaults` entries override declarative `<tbw-grid-type>` entries for the same type. - Type-level editors require the editing feature/plugin to be enabled in your runtime/framework package. `editor` and `editorParams` only appear on the `TypeDefault` type once the editing plugin's types are in scope. ### Custom Header Renderers Customize column header cells using `headerLabelRenderer` or `headerRenderer`. :::tip The framework adapters (React, Vue, Angular) bridge `headerRenderer` and `headerLabelRenderer` — you can use React JSX, Vue components/render functions, or Angular component classes just like cell renderers. Vanilla DOM functions also work. ::: **`headerLabelRenderer`** — Modify just the label portion of the header (sort icons and filter buttons are still managed by the grid): #### TypeScript ```typescript { field: 'name', header: 'Name', headerLabelRenderer: ({ value }) => { const span = document.createElement('span'); span.innerHTML = `${value} <span style="color:red">*</span>`; return span; }, } ``` #### React ```tsx // In GridConfig or ColumnConfig { field: 'name', header: 'Name', headerLabelRenderer: ({ value }) => ( <span>{value} <span style={{ color: 'red' }}>*</span></span> ), } ``` #### Vue ```ts // In GridConfig or ColumnConfig { field: 'name', header: 'Name', headerLabelRenderer: ({ value }) => h('span', [value, h('span', { style: 'color:red' }, ' *')]), } ``` #### Angular ```typescript // Using a component class in ColumnConfig { field: 'name', header: 'Name', headerLabelRenderer: RequiredLabelComponent, } // RequiredLabelComponent @Component({ selector: 'app-required-label', template: `{{ value() }} <span style="color:red">*</span>`, }) export class RequiredLabelComponent { value = input<string>(); column = input<unknown>(); } ``` **`headerRenderer`** — Full control over the entire header cell. You are responsible for rendering sort icons using `ctx.renderSortIcon()`: #### TypeScript ```typescript { field: 'email', header: 'Email', headerRenderer: (ctx) => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%'; const icon = document.createElement('span'); icon.textContent = '📧'; wrapper.appendChild(icon); const label = document.createElement('span'); label.textContent = ctx.value; label.style.flex = '1'; wrapper.appendChild(label); const sortIcon = ctx.renderSortIcon(); if (sortIcon) wrapper.appendChild(sortIcon); return wrapper; }, } ``` #### React ```tsx // In GridConfig or ColumnConfig // Use ctx.sortState to render your own sort indicator in JSX { field: 'email', header: 'Email', headerRenderer: (ctx) => ( <div style={{ display: 'flex', alignItems: 'center', gap: '6px', width: '100%' }}> <span>📧</span> <span style={{ flex: 1 }}>{ctx.value}</span> {ctx.sortState && ( <span className="sort-icon"> {ctx.sortState === 'asc' ? '▲' : '▼'} </span> )} </div> ), } ``` #### Vue ```ts // In GridConfig or ColumnConfig // Use ctx.sortState to render your own sort indicator { field: 'email', header: 'Email', headerRenderer: (ctx) => h('div', { style: 'display:flex;align-items:center;gap:6px;width:100%' }, [ h('span', '📧'), h('span', { style: 'flex:1' }, ctx.value), ctx.sortState ? h('span', { class: 'sort-icon' }, ctx.sortState === 'asc' ? '▲' : '▼') : null, ]), } ``` #### Angular ```typescript // Using a component class in ColumnConfig { field: 'email', header: 'Email', headerRenderer: EmailHeaderComponent, } // EmailHeaderComponent @Component({ selector: 'app-email-header', template: ` <div style="display:flex;align-items:center;gap:6px;width:100%"> <span>📧</span> <span style="flex:1">{{ value() }}</span> </div> `, }) export class EmailHeaderComponent { value = input<string>(); column = input<unknown>(); sortState = input<'asc' | 'desc' | null>(); filterActive = input<boolean>(); renderSortIcon = input<() => HTMLElement | null>(); renderFilterButton = input<() => HTMLElement | null>(); } ``` The [`HeaderCellContext`](https://toolboxjs.com/grid/api/core/interfaces/headercellcontext.md) gives you the label text, the column config, the current sort/filter state, and helpers to render the built-in sort icon and filter button when you want to keep them. ```ts // HeaderRenderersDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('#demo-header-renderers'); if (grid) { grid.columns = [ { field: 'id', header: 'ID', sortable: true, width: 60 }, { field: 'name', header: 'Name', sortable: true, resizable: true, headerLabelRenderer: ({ value }) => { const span = document.createElement('span'); span.innerHTML = `${value} <span style="color:red;font-weight:bold;">*</span>`; return span; }, }, { field: 'email', header: 'Email', sortable: true, resizable: true, headerRenderer: (ctx) => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;flex-direction:column;gap:4px;width:100%;padding:8px 0;'; const top = document.createElement('div'); top.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%;'; const icon = document.createElement('span'); icon.textContent = '📧'; top.appendChild(icon); const label = document.createElement('span'); label.textContent = ctx.value; label.style.flex = '1'; top.appendChild(label); const sortIcon = ctx.renderSortIcon(); if (sortIcon) top.appendChild(sortIcon); wrapper.appendChild(top); const caption = document.createElement('small'); caption.textContent = 'Primary contact'; caption.style.cssText = 'opacity:0.7;font-weight:400;'; wrapper.appendChild(caption); const hint = document.createElement('small'); hint.textContent = 'Verified addresses only'; hint.style.cssText = 'opacity:0.7;font-weight:400;'; wrapper.appendChild(hint); return wrapper; }, }, { field: 'score', header: 'Score', sortable: true, type: 'number', width: 80 }, ]; const names = [ 'Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Heidi', 'Ivan', 'Judy', 'Mallory', 'Niaj', 'Olivia', 'Peggy', ]; grid.rows = names.map((name, i) => ({ id: i + 1, name, email: `${name.toLowerCase()}@example.com`, score: 60 + ((i * 7) % 40), })); } ``` --- ## Column State Persistence The grid tracks column state — widths, sort direction, order, and visibility. You can save, load, and reset this state for user personalization. ### What State Contains `getColumnState()` returns an array of [`GridColumnState`](https://toolboxjs.com/grid/api/core/interfaces/gridcolumnstate.md) objects — one entry per column capturing its field, current width, sort direction and priority, and visibility. ### Listening for Changes The `column-state-change` event fires whenever the user resizes, reorders, sorts, or hides/shows columns: #### TypeScript ```typescript grid.on('column-state-change', (state) => { localStorage.setItem('my-grid-state', JSON.stringify(state)); }); ``` #### React ```tsx <DataGrid rows={rows} onColumnStateChange={(e) => { localStorage.setItem('my-grid-state', JSON.stringify(e.detail)); }} /> ``` #### Vue ```html <template> <TbwGrid :rows="rows" @column-state-change="onColumnStateChange" /> </template> <script setup lang="ts"> function onColumnStateChange(e: CustomEvent<GridColumnState[]>) { localStorage.setItem('my-grid-state', JSON.stringify(e.detail)); } </script> ``` #### Angular ```html <tbw-grid [rows]="rows" (column-state-change)="onColumnStateChange($event)" /> ``` ```typescript onColumnStateChange(e: CustomEvent<GridColumnState[]>) { localStorage.setItem('my-grid-state', JSON.stringify(e.detail)); } ``` ### Applying Saved State Restore a previously saved state using `applyColumnState()`: ```typescript const saved = localStorage.getItem('my-grid-state'); if (saved) { grid.applyColumnState(JSON.parse(saved)); } ``` ### Resetting State Re-assign the original column definitions to reset to defaults: ```typescript grid.columns = [...originalColumns]; ``` ```ts // ColumnStatePersistenceDemo.astro import '@toolbox-web/grid'; import type { ColumnConfig, GridColumnState } from '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const STORAGE_KEY = 'tbw-demo-column-state'; const container = document.getElementById('demo-column-state')?.closest('.grid-demo'); if (container) { const grid = queryGrid('tbw-grid', container)!; const status = container.querySelector<HTMLElement>('[data-status]')!; const log = container.querySelector<HTMLElement>('[data-log]')!; const defaultColumns: ColumnConfig[] = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 160 }, { field: 'department', header: 'Department', width: 140 }, { field: 'salary', header: 'Salary', type: 'number', width: 100 }, { field: 'location', header: 'Location', width: 130 }, ]; grid.columns = defaultColumns; grid.sortable = true; grid.resizable = true; grid.rows = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, location: 'New York' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 72000, location: 'London' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 108000, location: 'Berlin' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 67000, location: 'Tokyo' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 81000, location: 'New York' }, { id: 6, name: 'Frank Miller', department: 'Sales', salary: 73000, location: 'London' }, ]; function showStatus(msg: string) { status.textContent = msg; setTimeout(() => { status.textContent = ''; }, 2000); } function appendLog(msg: string) { log.textContent += msg + '\n'; log.scrollTop = log.scrollHeight; } grid.on('column-state-change', (detail) => { appendLog(`State changed: ${JSON.stringify(detail).slice(0, 120)}…`); }); container.querySelector('[data-save]')!.addEventListener('click', () => { const state = grid.getColumnState(); localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); showStatus('State saved!'); appendLog('Saved: ' + JSON.stringify(state, null, 2)); }); container.querySelector('[data-load]')!.addEventListener('click', () => { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) { showStatus('No saved state found.'); return; } const state: GridColumnState[] = JSON.parse(raw); grid.applyColumnState(state); showStatus('State loaded!'); appendLog('Loaded: ' + JSON.stringify(state, null, 2)); }); container.querySelector('[data-clear]')!.addEventListener('click', () => { localStorage.removeItem(STORAGE_KEY); showStatus('Saved state cleared.'); appendLog('Cleared localStorage.'); }); container.querySelector('[data-reset]')!.addEventListener('click', () => { grid.columns = [...defaultColumns]; showStatus('Grid reset to defaults.'); appendLog('Reset to default columns.'); }); } ``` ### Named Layout Profiles (Saved Views) What users usually ask for is not "column state" but **saved views**: _"give me my Q3 Review layout back"_. There is no `profiles` feature to enable — a profile is just a snapshot you assemble from the core column-state API plus the plugins that own the rest of the layout, stored under a name. | Part of the layout | Captured with | Restored with | | --- | --- | --- | | Order, width, visibility, sort | `grid.getColumnState()` | `grid.applyColumnState(state)` | | Filters | included in column state when [`filtering`](https://toolboxjs.com/grid/plugins/filtering.md) runs with `trackColumnState: true`, otherwise `getFilterModel()` | `applyColumnState`, otherwise `setFilterModel()` | | Multi-column sort | `multiSort.getSortModel()` | `multiSort.setSortModel(model)` | | Pinning | `pinnedColumns.getLeftPinnedColumns()` / `getRightPinnedColumns()` | `pinnedColumns.setPinPosition(field, position)` | | Row grouping | the group-by fields **your app** chose (the `groupOn` callback is a function and cannot be serialized) + `groupingRows.getExpandedGroups()` | `groupingRows.setGroupOn(fn, expandedKeys)` | ```typescript import { queryGrid } from '@toolbox-web/grid'; import type { GridColumnState } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); const STORE_KEY = 'employee-grid.profiles'; interface LayoutProfile { name: string; columns: GridColumnState; pinned: Record<string, 'left' | 'right'>; sort: { field: string; direction: 'asc' | 'desc' }[]; groupBy: string[]; expandedGroups: string[]; } function captureProfile(name: string, groupBy: string[]): LayoutProfile { const pinnedPlugin = grid.getPluginByName('pinnedColumns'); const pinned: LayoutProfile['pinned'] = {}; pinnedPlugin?.getLeftPinnedColumns().forEach((c) => (pinned[c.field] = 'left')); pinnedPlugin?.getRightPinnedColumns().forEach((c) => (pinned[c.field] = 'right')); return { name, groupBy, columns: grid.getColumnState(), pinned, sort: grid.getPluginByName('multiSort')?.getSortModel() ?? [], expandedGroups: grid.getPluginByName('groupingRows')?.getExpandedGroups() ?? [], }; } function applyProfile(profile: LayoutProfile): void { grid.applyColumnState(profile.columns); const pinnedPlugin = grid.getPluginByName('pinnedColumns'); grid.columns.forEach((c) => pinnedPlugin?.setPinPosition(c.field, profile.pinned[c.field])); grid.getPluginByName('multiSort')?.setSortModel(profile.sort); const grouping = grid.getPluginByName('groupingRows'); grouping?.setGroupOn( profile.groupBy.length ? (row) => profile.groupBy.map((f) => (row as Record<string, unknown>)[f]) : undefined, profile.expandedGroups, ); } // Storage is deliberately boring — swap localStorage for a `PUT /users/me/views` call // to make profiles follow the user across machines. function listProfiles(): Record<string, LayoutProfile> { return JSON.parse(localStorage.getItem(STORE_KEY) ?? '{}'); } function saveProfile(name: string, groupBy: string[] = []): void { localStorage.setItem( STORE_KEY, JSON.stringify({ ...listProfiles(), [name]: captureProfile(name, groupBy) }), ); } function loadProfile(name: string): void { const profile = listProfiles()[name]; if (profile) applyProfile(profile); } ``` :::tip `applyColumnState()` is a batch — pinning, sort and grouping are applied after it so they win over anything the restored column state implies. Reset to the shipped defaults by re-assigning `grid.columns = [...originalColumns]` before applying a profile, so a column added since the profile was saved doesn't inherit stale state. ::: --- ## Shell Components :::caution[The shell moved to a plugin] The shell (header bar, toolbar, and tool-panel sidebar) is **no longer part of core** — it is now the **[Shell plugin](https://toolboxjs.com/grid/plugins/shell.md)**. See that page for the full guide, demos, and configuration reference. The shell is **opt-in**: code that uses the header bar, toolbar, or tool panels **MUST** enable it explicitly: ```ts import '@toolbox-web/grid/features/shell'; grid.gridConfig = { features: { shell: { header: { title: 'Employee Data' } } }, }; ``` The `grid.register*` / `openToolPanel` element delegates were removed in v3 — use `grid.getPluginByName('shell')` instead — and shell types are imported from `@toolbox-web/grid/plugins/shell`. The `shell` configuration object itself is augmented onto `gridConfig` by the plugin. See [Migrating from the core shell](https://toolboxjs.com/grid/plugins/shell.md#migrating-from-the-core-shell-pre-v3). ::: --- ## Loading States The grid supports loading indicators at three levels: grid-wide, per-row, and per-cell. ### API Reference **Grid-level** — Shows a full overlay spinner: ```typescript grid.loading = true; try { grid.rows = await fetchRows(); } finally { grid.loading = false; // in `finally`, or a rejected fetch leaves the overlay up forever } ``` The `loading` attribute also works in HTML: `<tbw-grid loading></tbw-grid>`. **Row-level** — Shows a spinner on a specific row (requires `getRowId`): ```typescript grid.setRowLoading('row-42', true); // ... update row ... grid.setRowLoading('row-42', false); ``` **Cell-level** — Shows a spinner on a specific cell: ```typescript grid.setCellLoading('row-42', 'status', true); // ... update cell ... grid.setCellLoading('row-42', 'status', false); ``` **Query and clear:** ```typescript grid.isRowLoading('row-42'); // boolean grid.isCellLoading('row-42', 'name'); // boolean grid.clearAllLoading(); // remove all indicators ``` ```ts // LoadingStatesDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; interface Employee { id: string; name: string; email: string; department: string; } const container = document.getElementById('loading-states-container'); const grid = queryGrid<Employee>('tbw-grid', container!); if (container && grid) { const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry']; const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance']; const employees: Employee[] = Array.from({ length: 8 }, (_, i) => ({ id: `emp-${i + 1}`, name: `${names[i % names.length]} ${i + 1}`, email: `${names[i % names.length].toLowerCase()}${i + 1}@example.com`, department: departments[i % departments.length], })); grid.gridConfig = { getRowId: (row) => row.id }; grid.columns = [ { field: 'id', header: 'ID', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ]; grid.rows = employees; const hints: Record<string, string> = { grid: 'Click <strong>▶ Simulate</strong> to show full-grid loading overlay.', row: 'Click any <strong>row</strong> to trigger row loading.', cell: 'Click any <strong>cell</strong> to trigger cell loading.', }; const hintEl = container.querySelector('.loading-hint'); let currentMode: 'grid' | 'row' | 'cell' = 'grid'; let currentAutoReset = true; container.addEventListener('control-change', ((e: CustomEvent) => { const v = e.detail.allValues as { mode: string; autoReset: boolean }; currentMode = v.mode as 'grid' | 'row' | 'cell'; currentAutoReset = v.autoReset; if (hintEl) hintEl.innerHTML = hints[currentMode] ?? ''; }) as EventListener); // Row click → row loading grid.on('row-click', ({ row }) => { if (currentMode !== 'row') return; const timeout = currentAutoReset ? 1000 : null; if (timeout) { grid.setRowLoading(row.id, true); setTimeout(() => grid.setRowLoading(row.id, false), timeout); } else { grid.setRowLoading(row.id, !grid.isRowLoading(row.id)); } }); // Cell click → cell loading grid.on('cell-click', ({ row, column }) => { if (currentMode !== 'cell') return; const timeout = currentAutoReset ? 1000 : null; if (timeout) { grid.setCellLoading(row.id, column.field, true); setTimeout(() => grid.setCellLoading(row.id, column.field, false), timeout); } else { grid.setCellLoading(row.id, column.field, !grid.isCellLoading(row.id, column.field)); } }); // Simulate button container.querySelector('[data-loading="simulate"]')?.addEventListener('click', async () => { grid.clearAllLoading(); const timeout = currentAutoReset ? 1000 : null; if (currentMode === 'grid') { grid.loading = true; if (timeout) setTimeout(() => { grid.loading = false; }, timeout); } else if (currentMode === 'row') { for (const emp of employees) { grid.setRowLoading(emp.id, true); await new Promise((r) => setTimeout(r, 100)); } if (timeout) setTimeout(() => grid.clearAllLoading(), timeout); } else if (currentMode === 'cell') { for (const emp of employees) { grid.setCellLoading(emp.id, 'email', true); await new Promise((r) => setTimeout(r, 100)); } if (timeout) setTimeout(() => grid.clearAllLoading(), timeout); } }); } ``` ### Custom Loading Renderer Replace the default spinner with a custom element using `loadingRenderer`: #### TypeScript ```typescript grid.gridConfig = { loadingRenderer: (context) => { const el = document.createElement('div'); el.className = 'my-spinner'; // context.size is 'large' (grid-level) or 'small' (row/cell) el.style.width = context.size === 'large' ? '48px' : '16px'; el.style.height = el.style.width; return el; }, }; ``` #### React ```tsx import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const config: GridConfig = { loadingRenderer: ({ size }) => ( <div className="my-spinner" style={{ width: size === 'large' ? '48px' : '16px', height: size === 'large' ? '48px' : '16px', }} /> ), }; function MyGrid({ rows }) { return <DataGrid rows={rows} gridConfig={config} />; } ``` #### Vue ```html <script setup lang="ts"> import { h } from 'vue'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; // Option 1: Render function const config: GridConfig = { loadingRenderer: ({ size }) => h('div', { class: 'my-spinner', style: { width: size === 'large' ? '48px' : '16px', height: size === 'large' ? '48px' : '16px', }, }), }; // Option 2: Vue component (receives `size` prop) // import MySpinner from './MySpinner.vue'; // const config: GridConfig = { loadingRenderer: MySpinner }; </script> <template> <TbwGrid :rows="rows" :grid-config="config" /> </template> ``` #### Angular ```typescript import { Component, input } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; // Loading spinner component (receives `size` input) @Component({ selector: 'app-spinner', template: ` <div class="my-spinner" [style.width]="size() === 'large' ? '48px' : '16px'" [style.height]="size() === 'large' ? '48px' : '16px'"> </div> `, }) export class SpinnerComponent { size = input<'large' | 'small'>('large'); } @Component({ imports: [Grid], template: `<tbw-grid [rows]="rows" [gridConfig]="config" />`, }) export class MyGridComponent { config: GridConfig = { loadingRenderer: SpinnerComponent, }; } ``` The `LoadingContext` provides a `size` property: `'large'` for the grid-wide overlay (up to 48×48 px) and `'small'` for row/cell indicators (sized to the row height). ```ts // CustomLoadingRendererDemo.astro import { queryGrid } from '@toolbox-web/grid'; const grid = await queryGrid('#demo-custom-loading-renderer', true); const toggleBtn = document.querySelector<HTMLButtonElement>('[data-toggle="loading"]'); if (grid) { grid.columns = [ { field: 'id', header: 'ID', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ]; grid.rows = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing' }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', department: 'Engineering' }, { id: 4, name: 'Dan Wilson', email: 'dan@example.com', department: 'Sales' }, { id: 5, name: 'Eve Brown', email: 'eve@example.com', department: 'HR' }, ]; grid.registerStyles('custom-loading', ` .progress-bar-container { position: absolute; top: 0; left: 0; right: 0; height: 4px; background: light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.08)); overflow: hidden; z-index: 1000; } .progress-bar { height: 100%; background: light-dark(#1976d2, #64b5f6); width: 30%; animation: progress-indeterminate 2s cubic-bezier(0.4,0,0.2,1) infinite; transform-origin: left; } @keyframes progress-indeterminate { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } } /* WCAG 2.2.2: the loop runs for as long as the grid is loading, so under reduced motion fall back to a static full-width bar rather than a sliver frozen mid-travel. */ @media (prefers-reduced-motion: reduce) { .progress-bar { animation: none; width: 100%; } } `); grid.gridConfig = { getRowId: (row: { id: number }) => row.id, loadingRenderer: () => { const container = document.createElement('div'); container.className = 'progress-bar-container'; const bar = document.createElement('div'); bar.className = 'progress-bar'; container.appendChild(bar); return container; }, }; toggleBtn?.addEventListener('click', () => { grid.loading = !grid.loading; }); } ``` ### Empty State When the grid has no rows and is **not** loading, it shows a built-in message (`No data to display`, or `No matching rows` when a filter plugin hides every source row). Override the message with `emptyRenderer` — typically to surface error text from a failed fetch, or to provide an actionable empty state. ```typescript grid.gridConfig = { emptyRenderer: (ctx) => { if (loadError) return `Failed to load deals: ${loadError.message}`; if (ctx.filteredOut) return 'No deals match the current filter.'; return 'No deals to display.'; }, }; ``` The renderer receives an [`EmptyContext`](https://toolboxjs.com/grid/api/core/interfaces/emptycontext.md) with: - `sourceRowCount` — the number of input rows before any plugin filtering. - `filteredOut` — `true` when `sourceRowCount > 0` but all rows were hidden (e.g. by the [FilteringPlugin](https://toolboxjs.com/grid/plugins/filtering.md) or grouping). Return an `HTMLElement` for rich content (icons, buttons, multi-line layouts) or a `string` for plain text. Set `emptyRenderer: null` to suppress the overlay entirely. The overlay mounts inside the rows container by default. Set `emptyOverlay: 'grid'` to cover the entire grid (including the header) instead — useful for full-page error states. ```typescript grid.gridConfig = { emptyOverlay: 'grid', emptyRenderer: (ctx) => /* ... */, }; ``` :::note The loading overlay always wins: while `grid.loading === true` the empty overlay is hidden, even if there are zero rows. This avoids flashing an empty message during the initial fetch. ::: ```ts // EmptyStateDemo.astro import type { EmptyContext } from '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; type Row = { id: number; name: string; email: string; department: string }; const allRows: Array<Row> = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing' }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', department: 'Engineering' }, { id: 4, name: 'Dan Wilson', email: 'dan@example.com', department: 'Sales' }, { id: 5, name: 'Eve Brown', email: 'eve@example.com', department: 'HR' }, ]; const grid = await queryGrid('#demo-empty-state', true); const statusEl = document.querySelector<HTMLSpanElement>('[data-status]'); if (grid) { grid.columns = [ { field: 'id', header: 'ID', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ]; let errorMessage: string | null = null; const setStatus = (msg: string) => { if (statusEl) statusEl.textContent = msg; }; const emptyRenderer = (ctx: EmptyContext) => { if (errorMessage) { const wrapper = document.createElement('div'); wrapper.className = 'empty-error'; const icon = document.createElement('div'); icon.className = 'empty-error-icon'; icon.textContent = '⚠'; const title = document.createElement('div'); title.className = 'empty-error-title'; title.textContent = 'Failed to load data'; const detail = document.createElement('div'); detail.className = 'empty-error-detail'; detail.textContent = errorMessage; wrapper.append(icon, title, detail); return wrapper; } if (ctx.filteredOut) { return 'No employees match the current filter.'; } return 'No employees to display. Click "Load data" to fetch.'; }; grid.registerStyles('empty-state-demo', ` .empty-error { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; max-width: 320px; } .empty-error-icon { font-size: 28px; color: light-dark(#c62828, #ef9a9a); } .empty-error-title { font-weight: 600; color: light-dark(#c62828, #ef9a9a); } .empty-error-detail { font-size: 0.85em; color: light-dark(rgba(0,0,0,0.6), rgba(255,255,255,0.6)); } `); grid.gridConfig = { getRowId: (row: Row) => row.id, emptyRenderer, }; grid.rows = []; setStatus('Empty state — showing default message.'); document.querySelector('[data-action="load"]')?.addEventListener('click', () => { errorMessage = null; grid.rows = allRows; setStatus(`Loaded ${allRows.length} rows.`); }); document.querySelector('[data-action="clear"]')?.addEventListener('click', () => { errorMessage = null; grid.rows = []; setStatus('Cleared — showing empty message.'); }); document.querySelector('[data-action="error"]')?.addEventListener('click', () => { errorMessage = 'Network request timed out after 30s'; grid.rows = []; setStatus('Simulated error — showing custom error renderer.'); }); } ``` --- ## Variable Row Heights By default all rows share a fixed height (`--tbw-row-height`, default 28 px). You can configure per-row heights using a function. ### Configuration ```typescript // Fixed height for all rows grid.gridConfig = { rowHeight: 56 }; // Per-row height function grid.gridConfig = { rowHeight: (row, index) => (row.hasDetails ? 80 : 40), }; ``` If your function returns `undefined` for a row, the grid **auto-measures** that row's actual DOM height after rendering. ### How Auto-Measurement Works 1. The grid renders rows with an estimated height. 2. After paint, it reads `offsetHeight` for each rendered row. 3. Measured heights are cached and the position cache is rebuilt. 4. A `ResizeObserver` watches for late layout shifts (font loading, lazy images) and re-measures automatically. ### Row Identity for Height Caching Measured heights are cached using: - **`getRowId`** / `rowId` — preferred, survives sort/filter changes - **Object reference** (WeakMap) — fallback when no ID is configured Provide `getRowId` for best results when rows are re-sorted, filtered, or grouped. ### Plugin-Provided Heights Plugins can override row heights by implementing the `getRowHeight(row, index)` hook. The **MasterDetailPlugin** and **ResponsivePlugin** use this to provide expanded-row heights. ### Performance Considerations - **Fixed heights** are fastest — the grid can calculate positions with pure math. - **Function-based heights** add a per-row function call during position-cache rebuilds. - **Auto-measured heights** require a DOM read pass after rendering — avoid for 10 k+ row grids unless combined with `getRowId` caching. - When mixing fixed and auto-measured rows, return a number for most rows and `undefined` only for rows that need measurement. ```ts // VariableRowHeightDemo.astro import '@toolbox-web/grid'; import { queryGrid, type CellRenderContext } from '@toolbox-web/grid'; interface Employee { id: number; name: string; role: string; department: string; notes: string; } const container = document.getElementById('variable-row-height-demo'); const grid = queryGrid<Employee>('tbw-grid', container!); if (!grid) throw new Error('Grid not found'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'role', header: 'Role', width: 100 }, { field: 'department', header: 'Department', width: 120 }, { field: 'notes', header: 'Notes', renderer: (ctx: CellRenderContext<Employee>) => { const { row } = ctx; // For demonstration, render notes with a tooltip for tall rows const cell = document.createElement('div'); cell.textContent = row.notes; if (tallRowIds.has(row.id)) { cell.title = 'This row has extended notes'; cell.style.whiteSpace = 'pre-wrap'; cell.style.fontStyle = 'italic'; } return cell; } }, ], getRowId: (row) => String(row.id), rowHeight: (row) => { // Rows with ids in tallRowIds get a taller height if (tallRowIds.has(row.id)) return 56; return undefined; // use default height }, }; // Generate 150 rows — a handful have long notes that warrant taller rows const tallRowIds = new Set([5, 18, 42, 77, 130]); const departments = ['Engineering', 'Marketing', 'Sales', 'Support', 'Finance', 'HR', 'Legal', 'Design']; const roles = ['Manager', 'Senior', 'Junior', 'Lead', 'Intern', 'Director', 'VP', 'Analyst']; grid.rows = Array.from({ length: 150 }, (_, i) => { const id = i + 1; const isTall = tallRowIds.has(id); return { id, name: `Employee ${id}`, role: roles[i % roles.length], department: departments[i % departments.length], notes: isTall ? `This employee has extended notes that require a taller row. ` + `Additional context: performance review pending, cross-team ` + `collaboration active, mentoring two junior engineers.` : `Standard note for employee ${id}.`, }; }); ``` --- ## Events The grid dispatches standard `CustomEvent`s for user interactions — clicks, sort changes, column resizes, and more. Plugin-specific events (selection, editing, filtering, etc.) are also available when those plugins are active. For the complete event reference — including listening patterns per framework, cancelable events, and all plugin events — see the **[Events section](https://toolboxjs.com/grid/api-reference.md#events)** of the API Reference. --- ## Methods The `<tbw-grid>` element exposes public methods for programmatic control — data manipulation, focus management, column state persistence, custom styles, shell control, loading indicators, row animation, and more. Use `createGrid()` or `queryGrid()` for type-safe access. For the complete method reference — including signatures, return types, and factory functions — see the **[API Reference](https://toolboxjs.com/grid/api-reference.md#methods)** page. --- --- # Architecture > Internal architecture of @toolbox-web/grid — configuration system, render scheduler, virtualization, plugin lifecycle, and light DOM design. This page describes the internal architecture of `@toolbox-web/grid`. It's intended for contributors, plugin developers, and anyone who wants to understand how the grid works under the hood. ## Design Philosophy 1. **Light DOM** — No Shadow DOM. The grid renders directly into the element, allowing full CSS customization. 2. **Single Source of Truth** — All configuration converges into `effectiveConfig`, which is the only state read by rendering logic. 3. **Plugin-First** — Features like selection, editing, and filtering are plugins, not core code. This keeps the core small and tree-shakeable. 4. **Web Standards** — Built on Custom Elements, CSS Custom Properties, `adoptedStyleSheets`, and standard DOM APIs. 5. **Framework-Agnostic** — Pure TypeScript/HTML, no runtime framework dependencies. ## Component Overview ``` ┌───────────────────────────────────────────────────────┐ │ <tbw-grid> │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Light DOM │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │ Header Row │ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │ Rows Viewport (scrollable) │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ Spacer (virtual scroll height) │ │ │ │ │ │ │ ├─────────────────────────────────────┤ │ │ │ │ │ │ │ Visible Rows (row pool) │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────┘ ``` ## Component Lifecycle ``` constructor() └── Create internal state objects └── Initialize render scheduler connectedCallback() └── Parse light DOM children (<tbw-grid-column>, <tbw-grid-header>) └── Merge configuration (gridConfig + columns + fitMode + light DOM) └── Attach plugins (validate dependencies, call onAttach) └── Render header + rows └── Set up scroll listener, resize observer attributeChangedCallback() └── Map attribute → property setter disconnectedCallback() └── Abort disconnect signal (plugins clean up) └── Remove event listeners └── Disconnect resize observer ``` --- ## Configuration Architecture The grid follows a **single source of truth** pattern. All configuration inputs converge into one `effectiveConfig` object, which is then used for all rendering and behavior. ### Why Single Source of Truth? - **Predictable behavior**: One canonical config means no ambiguity about which setting applies - **Easy debugging**: Inspect `effectiveConfig` to see exactly what the grid is using - **Flexible input**: Users can configure via the method most convenient for their use case - **Plugin-friendly**: Plugins can read/modify config through one consistent interface ### Input Sources Users can configure the grid through multiple input methods: ```mermaid flowchart TB subgraph inputs["INPUT SOURCES"] direction TB A["gridConfig<br/>property"] B["columns<br/>property"] C["fitMode<br/>property"] subgraph lightdom["LIGHT DOM"] D["<tbw-grid-column><br/>field, header"] E["<tbw-grid-header><br/>title"] end end A --> F B --> F C --> F D --> F E --> F F["ConfigManager.merge()<br/><i>single merge point</i>"] F --> G["#effectiveConfig<br/><i>canonical config</i><br/>◀ SINGLE SOURCE OF TRUTH"] G --> H["DERIVED STATE<br/>_columns (processed)<br/>_rows (processed)"] ``` ### Precedence Rules When the same property is set via multiple sources, higher precedence wins: | Priority | Source | Example | | ----------- | --------------------- | -------------------------------------------- | | 1 (lowest) | `gridConfig` property | `grid.gridConfig = { fitMode: 'stretch' }` | | 2 | Light DOM elements | `<tbw-grid-column field="name">` | | 3 | `columns` property | `grid.columns = [{ field: 'name' }]` | | 4 | Inferred columns | (auto-detected from first row) | | 5 (highest) | Individual props | `grid.fitMode = 'fixed'` | > **Note:** HTML attributes (`rows`, `columns`, `grid-config`, `fit-mode`) invoke the corresponding property setters via `attributeChangedCallback` — they are not a separate precedence layer. ### HTML Attribute Configuration The grid supports JSON-serialized configuration via HTML attributes: ```html <tbw-grid rows='[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' columns='[{"field":"id","header":"ID"},{"field":"name","header":"Name"}]' fit-mode="stretch" > </tbw-grid> ``` Supported attributes: `rows`, `columns`, `grid-config`, `fit-mode`. ### Light DOM Configuration The grid parses these light DOM elements on connection: ```html <tbw-grid> <!-- Column definitions (→ effectiveConfig.columns) --> <tbw-grid-column field="name" header="Name" sortable></tbw-grid-column> <tbw-grid-column field="age" header="Age" type="number"></tbw-grid-column> <!-- Shell header (→ effectiveConfig.shell.header) --> <tbw-grid-header title="My Data Grid"> <tbw-grid-header-content> <span>Custom content here</span> </tbw-grid-header-content> <tbw-grid-tool-buttons> <button class="tbw-toolbar-btn" title="Refresh">🔄</button> </tbw-grid-tool-buttons> </tbw-grid-header> </tbw-grid> ``` ### Internal State Categories | Category | Example | Description | |----------|---------|-------------| | **Input Properties** | `#rows`, `#columns`, `#gridConfig`, `#fitMode` | Raw user input, stored as-is | | **Effective Config** | `#effectiveConfig` | Merged canonical config — single source of truth | | **Derived State** | `_columns`, `_rows` | Post-plugin-processing state used by rendering | | **Runtime State** | `#hiddenColumns`, `sortState` | User-driven state changes (hide column, sort, etc.) | **Key rule:** Rendering logic reads from `effectiveConfig` or derived state, never from input properties. --- ### Two-Layer Config Architecture ConfigManager implements a two-layer architecture to separate the **original configuration** (from sources) from **runtime mutations**: ```mermaid flowchart TB subgraph sources["SOURCES"] A["gridConfig"] B["columns"] C["fitMode"] D["Light DOM"] E["Shell State Maps"] end A --> F B --> F C --> F D --> F E --> F F["ConfigManager.merge()"] F -->|"sources changed"| G["#originalConfig<br/><i>(frozen, immutable)</i>"] G -->|"clone"| H["#effectiveConfig<br/><i>(mutable, runtime changes)</i>"] H -->|"user interaction"| I["Runtime Mutations<br/>hidden, width, sort order"] J["resetState()"] -->|"clone original → effective"| H ``` **Layer 1: Original Config (`#originalConfig`)** - Built from all sources via `#collectAllSources()` - Frozen after creation (`Object.freeze`) - Immutable — never modified after merge - Serves as the "reset point" for the effective config **Layer 2: Effective Config (`#effectiveConfig`)** - Deep cloned from original config - Mutable — runtime changes go here - Column visibility, widths, sort order, etc. - All rendering reads from this layer **Key Behaviors:** | Operation | What Happens | | ----------------------------------------- | ---------------------------------------------------------- | | Source changes (gridConfig, columns, etc.) | `markSourcesChanged()` → `merge()` rebuilds both layers | | Runtime mutation (hide column, resize) | Modify `effectiveConfig` only, `original` untouched | | `resetState()` | Clone `original` → `effective`, discarding runtime changes | | `collectState()` | Diff `effective` vs `original` to get user changes | **When Sources Change:** Sources are re-collected only when `#sourcesChanged` is `true` AND columns already exist: 1. Setting `gridConfig`, `columns`, `fitMode` → auto-marks sources changed 2. Setting Light DOM columns → auto-marks sources changed 3. Shell state updates → call `markSourcesChanged()` explicitly 4. `merge()` is a no-op if sources haven't changed AND columns exist 5. If no columns exist yet, `merge()` always runs (to allow inference from rows) This optimization prevents unnecessary rebuilds when `merge()` is called multiple times per frame. --- ## Rendering Pipeline All rendering flows through a centralized **RenderScheduler** that batches all work into a single `requestAnimationFrame` callback. This eliminates race conditions between different parts of the grid that previously scheduled independent RAFs. ### Render Scheduler Architecture ```mermaid flowchart TB subgraph sources["RENDER REQUESTS"] A["Property Change<br/>(rows, columns, gridConfig)"] B["Framework Adapter<br/>(React/Angular refreshColumns)"] C["ResizeObserver<br/>(container resize)"] D["Scroll Event<br/>(virtualization)"] E["Plugin Request<br/>(afterRender needs)"] end A --> F["RenderScheduler.requestPhase()"] B --> F C --> F D --> G["Direct Call<br/>(hot path)"] E --> F F --> H["Single RAF<br/>#flush()"] H --> I["Phase-Ordered Execution"] subgraph phases["EXECUTION ORDER (data dependencies)"] P1["1. mergeConfig<br/>(FULL/COLUMNS phase)"] P2["2. processRows<br/>(ROWS phase)"] P3["3. processColumns + updateTemplate<br/>(COLUMNS phase)"] P4["4. renderHeader<br/>(HEADER phase)"] P5["5. refreshVirtualWindow<br/>(VIRTUALIZATION phase)"] P6["6. afterRender hooks<br/>(STYLE phase)"] end I --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 ``` ### Render Phases Work is organized into ordered phases. Multiple requests merge to the **highest requested phase**: | Phase | Priority | Work Performed | |-------|----------|----------------| | `STYLE` | 1 | Plugin `afterRender()` hooks only | | `VIRTUALIZATION` | 2 | Recalculate virtual window (+ STYLE) | | `HEADER` | 3 | Re-render header row (+ VIRTUALIZATION) | | `ROWS` | 4 | Rebuild row model (+ HEADER) | | `COLUMNS` | 5 | Process columns, update CSS template (+ ROWS) | | `FULL` | 6 | Merge effective config (+ COLUMNS) | Higher phases implicitly cover all lower phases. Requesting `COLUMNS` when `ROWS` is already pending results in just `COLUMNS` executing. **Example**: If React adapter requests `COLUMNS` and ResizeObserver requests `VIRTUALIZATION` in the same frame, only `COLUMNS` phase runs (which includes all lower phases). ### Execution Order in `#flush()` ```typescript if (phase >= RenderPhase.COLUMNS) mergeConfig(); if (phase >= RenderPhase.ROWS) processRows(); if (phase >= RenderPhase.COLUMNS) processColumns(); if (phase >= RenderPhase.COLUMNS) updateTemplate(); if (phase >= RenderPhase.HEADER) renderHeader(); if (phase >= RenderPhase.VIRTUALIZATION) renderVirtualWindow(); if (phase >= RenderPhase.STYLE) afterRender(); ``` ### Intentional Bypasses Some operations intentionally bypass the scheduler for performance: | Operation | Reason | | ---------------------- | ------------------------------------------ | | Scroll rendering | Hot path — must be synchronous for 60fps | | Shell rebuild | Creates DOM structure, not content updates | | Row height measurement | One-time post-paint measurement | ### Debugging Renders The scheduler is held on a private field (`#scheduler`), so direct introspection from the console is not part of the public API. To trace render activity: - Use the browser **Performance** tab and record around the interaction — each `requestAnimationFrame` callback shows up with the phase work (`processRows`, `processColumns`, `renderHeader`, `renderVirtualWindow`, plugin `afterRender`). - Add `console.log` calls in your own plugin's hooks to confirm which phase fires after a given input. - The `source` argument passed to `requestPhase()` (e.g., `'applyGridConfigUpdate'`, `'resize-observer'`, `'plugin:requestRender'`) is preserved for future debug surfaces; the strings are visible in source files when grepping for `requestPhase(`. --- ## Virtualization ### Row Virtualization (Built-in) The grid maintains a "virtual window" — an offset and count representing which rows are visible: ``` Total rows: 10,000 Viewport: 400px, rowHeight: 28px → ~14 visible rows Overscan: 8 rows above + 8 below = 30 DOM rows in pool Scroll position: 5,000px → startIndex: 178 Rendered: rows 170–200 (30 rows in DOM) ``` - **Row pool**: DOM rows are reused, not created/destroyed on scroll - **Transform-based positioning**: Each row uses `transform: translateY()` for GPU-accelerated positioning - **Range-based updates**: Only rows entering/leaving the viewport get updated content For very small datasets (≤8 rows by default), virtualization is bypassed — the overhead isn't worth it. #### Massive datasets and the browser height cap Browsers cap a single element's rendered height at roughly **33.5M px** (Chromium's `2^25`; Firefox/Safari are similar). With the default `rowHeight: 34`, that's hit at about **986,895 rows**. Above the cap, the faux-vscroll spacer would silently truncate, leaving the tail of the dataset unreachable. To support datasets exceeding the cap (e.g. log viewers with 10M+ rows), the grid switches to **fractional scroll mapping**: - The spacer is clamped at `MAX_ELEMENT_HEIGHT_PX` (≈ 33.5M px). - The native `scrollTop` is treated as a position in the clamped spacer space and translated into a **virtual** offset in raw row-content space: ```text virtualScrollTop = scrollTop * (rawContentHeight − viewport) / (spacerHeight − viewport) ``` - `scrollToRow(N)` reverse-maps the row's offset back into the spacer's `scrollTop` so any row in the dataset is reachable programmatically. For datasets below the cap, mapping is identity — pixel-accurate behavior is unchanged. Above the cap, every row remains reachable via the scrollbar, `Ctrl+End`, and `scrollToRow(N)`. Keyboard cell navigation (`ArrowDown`/`ArrowUp`/`Tab`/`PageDown`/`PageUp`) also stays single-row accurate — focus moves by row index and the auto-scroll that keeps the focused cell visible routes through the mapping. The remaining caveat is **direct scroll input** (mouse wheel, scrollbar drag, `Space`/`Shift+Space`): one pixel of `scrollTop` corresponds to many rows above the cap, so a single wheel notch can skip past hundreds of rows. The mapping helpers (`MAX_ELEMENT_HEIGHT_PX`, `computeScrollMapping`, `toVirtualScrollTop`, `fromVirtualScrollTop`) are exported for plugins that maintain their own scroll-driven DOM state. ### Column Virtualization (Plugin) The `ColumnVirtualizationPlugin` applies the same window concept horizontally. Only columns visible in the horizontal scroll position are rendered. --- ## Plugin System & Feature Registry The plugin lifecycle, hooks, manifest schema, validated properties, communication channels, and the feature registry are documented under [Plugin Development → Architecture](https://toolboxjs.com/grid/plugin-development/architecture.md). That page also covers tree-shaking, dependency ordering, and the lazy-hook design that keeps the feature API zero-cost when unused. For a step-by-step tutorial on building your own plugin, see the [Authoring Guide](https://toolboxjs.com/grid/plugin-development/custom-plugins.md). --- ## Framework Adapter System How React, Angular, and Vue adapters intercept cell rendering, editor creation, config processing, and cell cleanup is documented under [Framework Adapters → Architecture](https://toolboxjs.com/grid/framework-adapters/architecture.md). --- ## Shell System The **shell** is an optional wrapper that adds a header bar, toolbar buttons, and a collapsible tool panel sidebar to the grid. ### Structure ``` ┌──────────────────────────────────────────────────────────┐ │ SHELL HEADER │ │ ┌──────────┬──────────────────┬────────────────────────┐ │ │ │ Title │ Header Content │ Toolbar │ ☰ Toggle │ │ │ └──────────┴──────────────────┴────────────────────────┘ │ ├───────────┬──────────────────────────────────────────────┤ │ TOOL │ │ │ PANEL │ GRID CONTENT │ │ (sidebar) │ │ │ ┌───────┐ │ ┌─────────────────────────────────────┐ │ │ │Section│ │ │ Header Row │ │ │ │ ▼ │ │ ├─────────────────────────────────────┤ │ │ │Content│ │ │ Data Rows (virtualized) │ │ │ │ │ │ │ │ │ │ ├───────┤ │ └─────────────────────────────────────┘ │ │ │Section│ │ │ │ │ ▶ │ │ │ │ └───────┘ │ │ └───────────┴──────────────────────────────────────────────┘ ``` ### Configuration The shell is configured via `gridConfig.shell` or Light DOM elements: ```typescript gridConfig: { shell: { header: { title: 'My Grid' }, toolPanel: { position: 'left' | 'right', // Sidebar position width: '17.5em', // Panel width (CSS value) defaultOpen: 'filters', // Section auto-expanded on first open (also opens the sidebar today — see #259) initialState: 'closed', // 'open' | 'closed' — preferred way to control sidebar open state locked: false, // true = sidebar always open, toggle hidden closeOnClickOutside: false, }, }, } ``` Or declaratively: ```html <tbw-grid> <tbw-grid-header title="My Grid"> <tbw-grid-header-content> <span>Custom center content</span> </tbw-grid-header-content> <tbw-grid-tool-buttons> <button class="tbw-toolbar-btn" title="Export">📥</button> </tbw-grid-tool-buttons> </tbw-grid-header> </tbw-grid> ``` ### Content Types Plugins and application code can register three types of content: | Type | Location | Example | |------|----------|---------| | **ToolPanel** | Accordion sections in sidebar | Filter panel, visibility panel, pivot config | | **HeaderContent** | Center of header bar | Search box, breadcrumbs | | **ToolbarContent** | Right side of header (before toggle) | Export button, view switcher | Each content type uses a render function pattern: ```typescript grid.getPluginByName('shell')?.registerToolPanel({ id: 'filters', title: 'Filters', icon: '🔍', render: (container) => { container.innerHTML = '<div>Filter UI here</div>'; return () => { /* cleanup */ }; }, }); ``` ### Shell State The shell maintains runtime state for: - **Panel open/close** — `isPanelOpen`, toggled via `openToolPanel()` / `closeToolPanel()` - **Expanded sections** — `expandedSections: Set<string>`, accordion state - **Content registrations** — `toolPanels`, `headerContents`, `toolbarContents` Maps - **Cleanup functions** — Each rendered content can return a cleanup callback, tracked for disposal ### Lazy Rendering Tool panel section content is rendered **lazily** — the `render()` function only executes when the user expands that accordion section for the first time. This avoids expensive initialization for panels the user never opens. --- ## Animation System The grid supports CSS-driven animations for row insert, remove, and update operations. ### Animation Types | Type | Trigger | Default Duration | CSS Variable | |------|---------|-----------------|--------------| | `'insert'` | `insertRow()`, bulk add | 300ms | `--tbw-row-insert-duration` | | `'remove'` | `removeRow()`, bulk delete | 200ms | `--tbw-row-remove-duration` | | `'change'` | `animateRow()`, data update highlight | 500ms | `--tbw-row-change-duration` | ### How It Works Animations use a **CSS attribute hook** pattern: 1. Grid sets `data-animating="insert"` on the row element 2. CSS transitions/keyframes in `grid.css` target `[data-animating="insert"]` 3. After the duration, a `setTimeout` removes the attribute (or the row for `'remove'`) 4. A `Promise` resolves when the animation completes ```typescript // Core mechanism (row-animation.ts) rowEl.setAttribute('data-animating', animationType); const duration = getAnimationDuration(rowEl, animationType); setTimeout(() => { if (animationType !== 'remove') { rowEl.removeAttribute('data-animating'); } onComplete?.(); }, duration); ``` ### Animation Configuration ```typescript gridConfig: { animation: { mode: 'reduced-motion', // Default: respects prefers-reduced-motion duration: 200, // Sets --tbw-animation-duration easing: 'ease-out', // Sets --tbw-animation-easing }, } ``` **`mode` options:** | Value | Behavior | |-------|----------| | `true` / `'on'` | Always animate | | `false` / `'off'` | Never animate | | `'reduced-motion'` | Respect `prefers-reduced-motion` media query (default) | ### Public API ```typescript // Highlight a row after data update await grid.animateRow(5, 'change'); // Highlight by row ID await grid.animateRowById('emp-123', 'change'); // Animate multiple rows const count = await grid.animateRows([0, 2, 5], 'change'); // Insert with auto-animation (default) await grid.insertRow(0, newRow); // Animates await grid.insertRow(0, newRow, false); // No animation // Remove with auto-animation (default) const removed = await grid.removeRow(5); // Animates fade-out const removed = await grid.removeRow(5, false); // Immediate ``` All methods return `Promise`s — `await` ensures the animation completes before proceeding. ### CSS Customization Override animation timing per-type via CSS custom properties: ```css tbw-grid { --tbw-row-change-duration: 800ms; /* Longer highlight */ --tbw-row-insert-duration: 0ms; /* Disable insert animation */ --tbw-row-remove-duration: 150ms; /* Faster fade-out */ --tbw-row-change-color: #fff3cd; /* Yellow highlight */ } ``` --- ## DOM Structure ```html <tbw-grid aria-label="..." role="grid"> <div class="data-grid-container"> <!-- Header --> <div role="rowgroup"> <div role="row" aria-rowindex="1"> <div role="columnheader" aria-colindex="1">Name</div> <div role="columnheader" aria-colindex="2">Age</div> </div> </div> <!-- Body (scrollable, virtualized) --> <div role="rowgroup" class="data-grid-body"> <div role="row" aria-rowindex="2" style="transform: translateY(0px)"> <div role="gridcell" aria-colindex="1">Alice</div> <div role="gridcell" aria-colindex="2">30</div> </div> </div> </div> </tbw-grid> ``` --- ## Styling Architecture ### CSS Custom Properties All styling uses CSS custom properties for theming. Override defaults to customize: ```css tbw-grid { --tbw-color-bg: #ffffff; --tbw-color-fg: #1a1a1a; --tbw-color-border: #e5e5e5; --tbw-color-header-bg: #f5f5f5; --tbw-row-height: 1.75em; /* ~28px at 16px font */ --tbw-header-height: 1.875em; /* ~30px at 16px font */ --tbw-font-family: system-ui, sans-serif; --tbw-font-size: 1em; } ``` ### Key CSS Architecture - **CSS Nesting**: Styles use `tbw-grid { .data-grid-container { ... } }` for scoping - **Cascade Layers**: `@layer tbw-base, tbw-plugins, tbw-theme` — user styles always win - **Adopted Stylesheets**: Dynamic styles use `document.adoptedStyleSheets` for efficiency — styles survive `replaceChildren()` calls - **`em`-Based Sizing**: Row height, padding, and spacing scale with `font-size` ### Plugin Styles Plugins inject CSS via their `styles` property using adopted stylesheets. They use a layered fallback pattern for flexibility: ```css /* Plugin-specific → Global fallback */ background: var(--tbw-selection-bg, var(--tbw-color-selection)); ``` --- ## Event Flow The grid dispatches `CustomEvent`s for every user-visible action (cell clicks, commits, sort changes, etc.). See the [Events section of the API Reference](https://toolboxjs.com/grid/api-reference.md#events) for the full list and the auto-generated [`DataGridEventMap`](https://toolboxjs.com/grid/api/core/interfaces/datagrideventmap.md) for payload types. Internally, plugins communicate through two mechanisms described under [Plugin Development → Plugin Communication](https://toolboxjs.com/grid/plugin-development/architecture.md#plugin-communication): the **Event Bus** (async, fire-and-forget) and the **Query System** (sync, request-response). Public `CustomEvent`s are dispatched by the core grid or by individual plugins after their internal processing completes. ### Data Flow Example: Sort ``` User clicks sort header ↓ Header click handler → update sortState ↓ Call processRows() on all plugins (MultiSortPlugin sorts) ↓ _rows updated with sorted order ↓ scheduler.requestPhase(ROWS, 'sort') ↓ requestAnimationFrame ↓ Render: update visible row content from new _rows ``` --- ## Performance ### DOM Optimization | Technique | Benefit | | --- | --- | | **Template Cloning** | 3–4× faster than `createElement` | | **Direct DOM Construction** | Avoids innerHTML parsing overhead | | **DocumentFragment** | Single reflow per row | | **Row Pooling** | Zero allocation during scroll | | **Cached DOM Refs** | Avoid querySelector per scroll | ### Rendering Pipeline | Technique | Benefit | | --- | --- | | **Centralized Scheduler** | Eliminates race conditions | | **Phase-Based Execution** | No duplicate work | | **adoptedStyleSheets** | Runtime CSS injection without child node removal | | **Idle Scheduling** | Faster time-to-interactive | | **Fast-Path Patching** | Skip expensive template logic for plain text | | **Cell Display Cache** | Avoid recomputing during scroll | ### Event Handling | Technique | Benefit | | --- | --- | | **Event Delegation** | Constant memory regardless of rows | | **Pooled Scroll Events** | Zero GC pressure during scroll | | **AbortController Cleanup** | No memory leaks on disconnect | --- --- # API Reference > Complete reference for the <tbw-grid> component — properties, methods, events, CSS custom properties, keyboard shortcuts, declarative configuration, and accessibility. Complete reference for the `<tbw-grid>` component API. ## Element Tag ```html <tbw-grid></tbw-grid> ``` The custom element is registered as `tbw-grid` (Toolbox Web Grid). ### Element type aliases The element class is [`DataGridElement<TRow>`](https://toolboxjs.com/grid/api/core/classes/datagridelement.md). Two aliases of the same class are exported for convenience — they are identical, so pick whichever reads better in your codebase: | Export | Kind | Equivalent to | | ------------- | ------------ | -------------------- | | `GridElement` | value + type | `DataGridElement` | | `TbwGrid` | type only | `DataGridElement` | ```ts import { createGrid, type TbwGrid } from '@toolbox-web/grid'; const grid: TbwGrid<Employee> = createGrid(); ``` ## Properties ### Core Data Properties | Property | Type | Default | Description | | ------------ | ---------------- | ------- | -------------------------- | | `rows` | `T[]` | `[]` | Array of row data objects | | `columns` | [`ColumnConfig[]`](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md) | `[]` | Column configuration array | | `gridConfig` | [`GridConfig<T>`](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md) | `{}` | Full configuration object | ### Layout Properties | Property | Type | Default | Description | | --------- | ---------------------- | ----------- | ---------------------- | | `fitMode` | `'stretch' \| 'fixed'` | `'stretch'` | Column sizing strategy | ### State Properties | Property | Type | Default | Description | | ------------- | --------------------------- | ------- | ------------------------------------------- | | `loading` | `boolean` | `false` | Grid-level loading state. Customise the indicator via [`gridConfig.loadingRenderer`](https://toolboxjs.com/grid/api/core/types/loadingrenderer.md) | | `columnState` | [`GridColumnState[]`](https://toolboxjs.com/grid/api/core/interfaces/gridcolumnstate.md) | - | Get/set column widths, order, visibility, sort | ### Read-only Properties | Property | Type | Description | | ------------- | ---------------------------- | ----------------------------- | | `changedRows` | `T[]` | Rows with pending edits (requires EditingPlugin) | | `sortState` | <code>Map<string, <a href="/grid/api/core/interfaces/sortstate/">SortState</a>></code> | Current sort state per column | ## Declarative Configuration The grid supports two forms of declarative HTML configuration: 1. **HTML Attributes** — JSON-serialized values on the `<tbw-grid>` element itself 2. **Light DOM Elements** — Child elements nested inside `<tbw-grid>` for more readable markup Both approaches can be combined. Light DOM elements take precedence over JSON attributes for the same configuration (e.g., `<tbw-grid-column>` elements override the `columns` attribute). :::note JavaScript property assignment always takes precedence over declarative configuration. Removing an attribute or element does not clear a JS-set value. ::: ### HTML Attributes Attributes on the `<tbw-grid>` element for simple or programmatically-generated configuration: | Attribute | Maps to Property | Type | Description | | ------------- | ---------------- | ------ | -------------------------- | | `rows` | `rows` | JSON | Row data as JSON array | | `columns` | `columns` | JSON | Column config as JSON array| | `grid-config` | `gridConfig` | JSON | Full config object as JSON | | `fit-mode` | `fitMode` | string | `'stretch'` or `'fixed'` | ```html <tbw-grid rows='[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' columns='[{"field":"id","header":"ID"},{"field":"name","header":"Name"}]' fit-mode="stretch" ></tbw-grid> ``` ### Plugin Host Attributes (`data-*`) Some plugins read their own `data-*` attributes directly from the `<tbw-grid>` host element when they attach. These are namespaced under `data-` to avoid colliding with the grid's own reactive attributes (`rows`, `columns`, `grid-config`, `fit-mode`, `loading`). They are read **once at attach time** — changing them afterward has no effect (use the JSON `grid-config` attribute or a JS property for reactive updates). | Attribute | Owning plugin | Description | | ---------- | ------------------ | -------------------------------------------------------------------------------------------------- | | `data-src` | [`ServerSidePlugin`](https://toolboxjs.com/grid/plugins/server-side.md) | URL to fetch row data from — enables a zero-JS server-fetched grid. See [Server-Side → Declarative `data-src`](https://toolboxjs.com/grid/plugins/server-side.md#declarative-data-src-zero-js). | ### Light DOM Elements Child elements inside `<tbw-grid>` for more readable, template-friendly configuration. Preferred for framework templates (Angular, Vue, etc.) and complex column setups with custom renderers/editors. #### Column Elements ##### `<tbw-grid-column>` Define column configuration declaratively. | Attribute | Type | Description | | -------------- | --------- | ---------------------------------------------------------------- | | `field` | `string` | **Required.** Data field key. Supports the `field:type` shorthand — e.g. `field="price:number"` expands to `field="price" type="number"` and an auto-generated `header` ("Price"). | | `header` | `string` | Column header text | | `type` | `string` | Column type. Built-ins: `'string'`, `'number'`, `'boolean'`, `'date'`, `'select'`. Any custom/plugin-registered type string is also accepted. | | `width` | `string` | Column width as a CSS grid track — `"120"` (px), `"100px"`, `"20%"`, `"2fr"`, `"minmax(120px, 1fr)"`, `"max-content"` | | `min-width` | `number` | Minimum column width in pixels (pixels only). Applies when `width` is omitted, and floors drag-resize. | | `sortable` | `boolean` | Enable sorting (presence = true) | | `resizable` | `boolean` | Enable resizing (presence = true) | | `order` | `number` | Initial column position (0-based index; must be non-negative integer). Columns without `order` fill positions in declaration order. Only affects initial render; user reorder takes precedence. | | `editable` | `boolean` | Enable editing (presence = true, requires EditingPlugin) | | `options` | `string` | Select options: `"value1:Label1,value2:Label2"` or `"val1,val2"` | | `pinned` | `string` | Pin the column to an edge: `'left'` / `'start'` or `'right'` / `'end'` (requires PinnedColumnsPlugin). | | `hidden` | `boolean` | Hide the column initially (presence = true; `hidden="false"` keeps it visible). Requires VisibilityPlugin. | | `lock-visible` | `boolean` | Prevent the column from being hidden via the visibility panel (presence = true). Requires VisibilityPlugin. | > Plugin-contributed attributes (`pinned`, `hidden`, `lock-visible`, `editable`, `<tbw-grid-column-editor>`) are read by their owning plugin when it is registered. They set the column's **initial** state — subsequent runtime changes (e.g. `setColumnVisible()`, drag-to-pin) take precedence. ```html <tbw-grid> <tbw-grid-column field="id" header="ID" type="number" width="80" order="0"></tbw-grid-column> <tbw-grid-column field="name" header="Name" sortable resizable order="1"></tbw-grid-column> <tbw-grid-column field="role" type="select" options="admin:Admin,user:User" order="2"></tbw-grid-column> </tbw-grid> ``` > **Column shorthand.** `field="salary:number"` is equivalent to `field="salary" type="number"` with an auto-generated `header`. > > - Recognized suffixes: `string`, `number`, `boolean`, `date`, `datetime`, `currency`. > - Only `string`, `number`, `boolean`, and `date` are `PrimitiveColumnType` built-ins. `datetime` and `currency` expand to those type strings, but only change rendering when a matching `typeDefaults` entry or plugin is registered. > - Any other colon-containing value is treated as a literal field name. > - The shorthand also works in the JSON `columns` attribute: `<tbw-grid columns='["id:number", "name", "email"]'>`. > - A non-empty explicit `type`/`header` attribute wins over the shorthand-derived value. ##### `<tbw-grid-column-view>` Custom view template inside a column. Content is used as the cell renderer. ```html <tbw-grid-column field="status"> <tbw-grid-column-view> <span class="badge">{{ value }}</span> </tbw-grid-column-view> </tbw-grid-column> ``` ##### `<tbw-grid-column-editor>` Custom editor template inside a column (requires EditingPlugin). ```html <tbw-grid-column field="name" editable> <tbw-grid-column-editor> <input type="text" /> </tbw-grid-column-editor> </tbw-grid-column> ``` ##### `<tbw-grid-column-header>` Custom header template inside a column. ```html <tbw-grid-column field="status"> <tbw-grid-column-header> <strong>Status</strong> <span class="required">*</span> </tbw-grid-column-header> </tbw-grid-column> ``` #### Shell Elements ##### `<tbw-grid-header>` Configure the shell header bar. | Attribute | Type | Description | | --------- | -------- | ------------------------ | | `title` | `string` | Grid title (left side) | ```html <tbw-grid> <tbw-grid-header title="Employee Directory"> <tbw-grid-header-content> <span>20 employees</span> </tbw-grid-header-content> </tbw-grid-header> </tbw-grid> ``` ##### `<tbw-grid-header-content>` Custom content in the shell header center area. Must be nested inside `<tbw-grid-header>`. ```html <tbw-grid-header title="My Grid"> <tbw-grid-header-content> <input type="search" placeholder="Search..." /> </tbw-grid-header-content> </tbw-grid-header> ``` ##### `<tbw-grid-tool-buttons>` Container for toolbar buttons (right side of shell header). ```html <tbw-grid> <tbw-grid-tool-buttons> <button class="tbw-toolbar-btn" title="Export">📥</button> <button class="tbw-toolbar-btn" title="Print">🖨️</button> </tbw-grid-tool-buttons> </tbw-grid> ``` #### Tool Panel Elements ##### `<tbw-grid-tool-panel>` Define a custom tool panel for the sidebar. | Attribute | Type | Description | | --------- | -------- | ---------------------------------------------- | | `id` | `string` | **Required.** Unique panel identifier | | `title` | `string` | Panel title in accordion header. Optional — omit when the panel content provides its own heading. When omitted for the only registered panel, the accordion header row is skipped entirely; with multiple panels a title-less header still renders (keeping the expand/collapse toggle) | | `icon` | `string` | Icon for accordion header (emoji or text) | | `tooltip` | `string` | Tooltip for accordion header | | `order` | `number` | Panel order priority (lower = first, default: 100) | ```html <tbw-grid> <tbw-grid-tool-panel id="filters" title="Filters" icon="🔍" order="10"> <div class="filter-panel"> <label>Status: <select>...</select></label> </div> </tbw-grid-tool-panel> </tbw-grid> ``` #### Plugin-Specific Elements ##### `<tbw-grid-detail>` (MasterDetailPlugin) Define the detail panel template for master-detail rows. Requires the MasterDetailPlugin. | Attribute | Type | Description | | --------------------- | ----------------------------- | ---------------------------------------------------- | | `animation` | `'slide' \| 'fade' \| false` | Panel expand/collapse animation (default: `'slide'`) | | `show-expand-column` | `boolean` | Show expand/collapse column (default: `true`) | | `expand-on-row-click` | `boolean` | Expand when row clicked (default: `false`) | | `height` | `number \| 'auto'` | Panel height in pixels or auto (default: `'auto'`) | ```html <tbw-grid> <tbw-grid-detail animation="slide" expand-on-row-click="true"> <div class="detail-panel"> <h3>{{ row.name }}</h3> <p>{{ row.description }}</p> </div> </tbw-grid-detail> </tbw-grid> ``` See the [MasterDetailPlugin documentation](https://toolboxjs.com/grid/plugins/master-detail.md) for more details. ##### `<tbw-grid-responsive-card>` (ResponsivePlugin) Define a custom card template for responsive mode. Requires the ResponsivePlugin. | Attribute | Type | Description | | ----------------- | -------------------- | ------------------------------------------------------- | | `breakpoint` | `number` | Width threshold in pixels for responsive mode | | `card-row-height` | `number \| 'auto'` | Card height in pixels or auto (default: `'auto'`) | | `hidden-columns` | `string` | Comma-separated field names to hide in card mode | | `hide-header` | `boolean` | Hide header row in responsive mode (default: `true`) | | `debounce-ms` | `number` | Resize debounce delay in ms (default: `100`) | ```html <tbw-grid> <tbw-grid-responsive-card breakpoint="500" card-row-height="80" hidden-columns="createdAt, updatedAt"> <div class="custom-card"> <strong>{{ row.name }}</strong> <span>{{ row.email }}</span> </div> </tbw-grid-responsive-card> </tbw-grid> ``` See the [ResponsivePlugin documentation](https://toolboxjs.com/grid/plugins/responsive.md) for more details. :::note Framework adapters may provide wrapper components for these elements. For example, `@toolbox-web/grid-react` provides `<GridResponsiveCard>` and `<GridDetailPanel>` React components with render prop support. See the [React adapter docs](https://toolboxjs.com/grid/react/getting-started.md) for details. ::: ## Helper Functions ```typescript import { createGrid, queryGrid } from '@toolbox-web/grid'; // Create a new grid element with configuration const grid = createGrid<Employee>({ columns: [{ field: 'name' }, { field: 'email' }], fitMode: 'stretch', }); document.body.appendChild(grid); grid.rows = employees; // Query an existing grid with type safety const existingGrid = queryGrid<Employee>('#my-grid'); if (existingGrid) { existingGrid.rows = newData; } ``` ## Methods ### Data Methods ```typescript // Update row data grid.rows = newData; // Wait for grid to be ready await grid.ready(); // Force layout recalculation await grid.forceLayout(); // Get current configuration (readonly) const config = await grid.getConfig(); ``` ### Row Update API The Row Update API provides ID-based access to individual rows for reading and updating data. This is especially useful when you need to update rows after external changes (e.g., API responses, WebSocket events) without replacing the entire dataset. #### Row Identification The grid automatically determines row IDs using these sources (in order of precedence): 1. `getRowId` function in gridConfig (custom ID resolution) 2. `id` property on the row object 3. `_id` property on the row object :::note Rows without any of these will not be accessible via the Row Update API. Calls to `getRow()` or `updateRow()` for such rows will return `undefined` or have no effect. ::: ```typescript // Configure custom row ID resolution grid.gridConfig = { columns: [...], getRowId: (row) => row.employeeId, // Use a custom field as ID }; // Get a row's ID const id = grid.getRowId(row); console.log(id); // "EMP-123" ``` #### Reading Rows ```typescript // Get a single row by its ID const employee = grid.getRow('EMP-123'); if (employee) { console.log(employee.name); } // Returns undefined if the row is not found const missing = grid.getRow('unknown-id'); console.log(missing); // undefined ``` #### Updating Rows ```typescript // Update a single row by ID grid.updateRow('EMP-123', { status: 'active', salary: 75000 }); // Batch update multiple rows grid.updateRows([ { id: 'EMP-123', changes: { status: 'active' } }, { id: 'EMP-456', changes: { department: 'Engineering' } }, ]); // Specify the update source (default: 'api') // Valid sources: 'user' | 'cascade' | 'api' | 'history' // ('history' is reserved for undo/redo re-application — it applies the value // but does not record a fresh history entry.) grid.updateRow('EMP-123', { status: 'inactive' }, 'api'); grid.updateRows(updates, 'api'); ``` :::note When the **EditingPlugin** is loaded (and the grid has a `getRowId`), `updateRow()` / `updateRows()` route each changed cell through the edit commit pipeline — so programmatic updates participate in **dirty tracking**, **undo/redo**, **validation**, and **cascades**, and fire `cell-commit` per cell (a `preventDefault()` handler vetoes the change). Without the EditingPlugin the value is applied directly. ::: #### Listening to Row Updates The `cell-change` event fires whenever row data is updated via the Row Update API: ```typescript grid.on('cell-change', ({ rowId, changes, source, row }) => { console.log(`Row ${rowId} updated via ${source}:`); console.log('Changes:', changes); // { status: 'active' } console.log('Full row:', row); // Complete row object after update }); ``` :::note The `cell-change` event is distinct from `cell-commit`, which fires during inline editing. Use `cell-change` for programmatic updates via `updateRow()`/`updateRows()`. ::: ### Column Methods ```typescript // Get all columns with visibility info const columns = grid.getAllColumns(); // Returns: Array<{ field, header, visible, lockVisible? }> // Show/hide columns grid.setColumnVisible('fieldName', false); grid.toggleColumnVisibility('fieldName'); grid.showAllColumns(); // Check column visibility const isVisible = grid.isColumnVisible('fieldName'); // Reorder columns grid.setColumnOrder(['id', 'name', 'email']); const order = grid.getColumnOrder(); ``` ### Editing Methods Bulk-edit lifecycle, change tracking, and active-row controls are provided by the **Editing plugin** and surfaced on the grid instance when the plugin is loaded. See [Editing Plugin → Imperative Bulk-Edit API](https://toolboxjs.com/grid/plugins/editing.md#imperative-bulk-edit-api) for the full method list and examples. ### Plugin Methods | Method | Returns | Description | | ------------------------ | ------------------------------ | ------------------------------------------------------ | | `getPluginByName(name)` | `Plugin \| undefined` | Get plugin instance by name — **preferred** (type-safe, no import needed) | | `getPlugin(PluginClass)` | `P \| undefined` | Get plugin instance by class (requires import) | `getPluginByName` is type-safe for all built-in plugins via the `PluginNameMap` interface — TypeScript automatically returns the correct plugin type (e.g., `SelectionPlugin` for `'selection'`). ```typescript // Preferred — type-safe, no import needed const selection = grid.getPluginByName('selection'); selection?.selectAll(); // ✅ SelectionPlugin methods are available // Alternative — get by class (requires plugin import) import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection'; const sel = grid.getPlugin(SelectionPlugin); // Check if plugin is registered if (selection) { selection.selectAll(); } ``` ### Custom Styles API The grid uses **light DOM** — standard CSS targeting elements inside `<tbw-grid>` works normally (global stylesheets, `<style>` in `<head>`, external CSS files). For **programmatic runtime styles**, use `registerStyles()` which injects CSS via `adoptedStyleSheets`: ```typescript // Inject styles programmatically at runtime grid.registerStyles('my-id', '.my-class { color: blue; }'); // Remove when no longer needed grid.unregisterStyles('my-id'); // List currently registered style IDs const styleIds = grid.getRegisteredStyles(); ``` :::caution Do not place `<style>` elements as **children of `<tbw-grid>`** — the grid calls `replaceChildren()` during renders, which removes child nodes. This only affects `<style>` tags placed *inside* the grid element itself. External stylesheets, `<style>` in `<head>`, and `<link>` tags are completely unaffected. ::: ### Insert & Remove Rows When you set `rows`, the grid automatically re-applies all active processing — sorting, filtering, grouping — so the data stays consistent. This is the right behavior for data refreshes (e.g., API responses), but **not** for manual row insertion or removal where you want the new row to stay exactly where you placed it. `insertRow(index, row)` and `removeRow(index)` operate directly on the current view without running the plugin pipeline. The row is also added to / removed from the source data so that the next full pipeline run includes it correctly. Both methods auto-animate by default (`'insert'` / `'remove'` animation). Pass `false` as the last argument to skip animation. They return `Promise`s that resolve when the animation completes. ```typescript // Insert a row at visible index 3 (auto-animates) grid.insertRow(3, newEmployee); // Insert without animation grid.insertRow(3, newEmployee, false); ``` ```typescript // Remove with fade-out animation (default) — await to ensure removal await grid.removeRow(5); // Remove immediately, no animation await grid.removeRow(5, false); ``` :::tip **When to use:** Only for user-initiated row insertion/removal where position matters. On the next `grid.rows = freshData` assignment, sort and filter re-apply normally. **When NOT to use:** When receiving fresh data from an API, WebSocket, or any external source. In those cases, just set `grid.rows = newData` and let the grid re-sort and re-filter. ::: ### Row Animation API All animation methods return `Promise`s that resolve when the animation completes. ```typescript import type { RowAnimationType } from '@toolbox-web/grid'; // Animate a single row by index (await to know when done) await grid.animateRow(rowIndex, 'change'); // 'insert' | 'change' | 'remove' // Animate a row by its ID await grid.animateRowById('EMP-123', 'insert'); // Animate multiple rows at once await grid.animateRows([0, 1, 2], 'change'); ``` ### Loading States ```typescript // Grid-level loading grid.loading = true; grid.loading = false; // To customise the loading indicator, configure a renderer at config time: grid.gridConfig = { loadingRenderer: (ctx) => { const el = document.createElement('div'); el.textContent = ctx.size === 'large' ? 'Loading…' : '…'; return el; }, }; // Row-level loading (by row ID, not index) grid.setRowLoading('emp-123', true); grid.setRowLoading('emp-123', false); // Cell-level loading (by row ID and field name) grid.setCellLoading('emp-123', 'email', true); grid.setCellLoading('emp-123', 'email', false); ``` ### Column State Persistence ```typescript // Get current column state (for saving) const state = grid.getColumnState(); localStorage.setItem('gridState', JSON.stringify(state)); // Restore column state const saved = localStorage.getItem('gridState'); if (saved) { grid.applyColumnState(JSON.parse(saved)); } // Reset to initial configuration grid.resetColumnState(); ``` ### Shell Methods (Header/Toolbar) The header bar, toolbar content, and tool panels are owned by the built-in **shell plugin**. Access it via `grid.getPluginByName('shell')` — the old `grid.*` element delegates were removed in v3: ```typescript const shell = grid.getPluginByName('shell'); // Register a tool panel shell?.registerToolPanel({ id: 'myPanel', title: 'My Panel', icon: '⚙️', render: (container) => { container.innerHTML = '<div>Panel content</div>'; }, }); // Open/close/toggle the tool panel sidebar shell?.openToolPanel(); shell?.closeToolPanel(); shell?.toggleToolPanel(); // Open directly on a specific accordion section (one-click navigation) shell?.openToolPanel('myPanel'); // Toggle specific accordion sections shell?.toggleToolPanelSection('myPanel'); // Check panel state const isOpen = shell?.isToolPanelOpen; const sections = shell?.expandedToolPanelSections; // Get registered panels const panels = shell?.getToolPanels(); ``` ### Header & Toolbar Content Register custom content in the grid's header bar (above column headers) or toolbar area. In vanilla you call the shell plugin's imperative `registerHeaderContent` / `registerToolbarContent` APIs directly; the framework adapters wrap those same APIs in declarative components/directives that mount your own component (with full reactivity and DI) into the slot — prefer those over reaching for the imperative API by hand. #### TypeScript ```typescript const shell = grid.getPluginByName('shell'); // Register header content (e.g., a search box) shell?.registerHeaderContent({ id: 'global-search', order: 10, render: (container) => { const input = document.createElement('input'); input.type = 'search'; input.placeholder = 'Search all columns...'; container.appendChild(input); }, }); // Unregister header content shell?.unregisterHeaderContent('global-search'); // Get all registered header content definitions const headerContents = shell?.getHeaderContents(); // Register toolbar content (e.g., action buttons) shell?.registerToolbarContent({ id: 'export-buttons', order: 100, render: (container) => { const btn = document.createElement('button'); btn.textContent = 'Export CSV'; btn.className = 'tbw-toolbar-btn'; container.appendChild(btn); }, }); // Unregister toolbar content shell?.unregisterToolbarContent('export-buttons'); // Get all registered toolbar content definitions const toolbarContents = shell?.getToolbarContents(); ``` #### React ```tsx import { DataGrid, GridHeaderContent, GridToolbarContent } from '@toolbox-web/grid-react'; function EmployeeGrid({ rows }) { return ( <DataGrid rows={rows}> <GridHeaderContent id="global-search" order={10}> <input type="search" placeholder="Search all columns..." /> </GridHeaderContent> <GridToolbarContent id="export-buttons" order={100}> <button className="tbw-toolbar-btn">Export CSV</button> </GridToolbarContent> </DataGrid> ); } ``` #### Vue ```html <script setup lang="ts"> import { TbwGrid, TbwGridHeaderContent, TbwGridToolbarContent } from '@toolbox-web/grid-vue'; </script> <template> <TbwGrid :rows="rows"> <TbwGridHeaderContent id="global-search" :order="10"> <input type="search" placeholder="Search all columns..." /> </TbwGridHeaderContent> <TbwGridToolbarContent id="export-buttons" :order="100"> <button class="tbw-toolbar-btn">Export CSV</button> </TbwGridToolbarContent> </TbwGrid> </template> ``` #### Angular ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { GridHeaderContent, GridToolbarContent } from '@toolbox-web/grid-angular/features/shell'; @Component({ imports: [Grid, GridHeaderContent, GridToolbarContent], template: ` <tbw-grid [rows]="rows"> <tbw-grid-header-content id="global-search" [order]="10"> <ng-template> <input type="search" placeholder="Search all columns..." /> </ng-template> </tbw-grid-header-content> <tbw-grid-toolbar-content id="export-buttons" [order]="100"> <ng-template> <button class="tbw-toolbar-btn">Export CSV</button> </ng-template> </tbw-grid-toolbar-content> </tbw-grid> `, }) export class EmployeeGridComponent { rows = [/* ... */]; } ``` ### Row Height ```typescript // Get the default row height in pixels // For fixed heights: the configured or CSS-measured row height // For variable heights: the average/estimated row height const height = grid.defaultRowHeight; ``` ### Focus Management When building custom editors that render outside the grid DOM (overlays, datepickers, dropdowns appended to `<body>`), use the focus container registry so the grid treats focus inside those elements as "still in the grid". ```typescript // Register an overlay panel so focus moving into it doesn't close the editor const panel = document.createElement('div'); document.body.appendChild(panel); grid.registerExternalFocusContainer(panel); // When the overlay is torn down, unregister it grid.unregisterExternalFocusContainer(panel); // Check whether focus is logically inside the grid (own DOM + external containers) const hasFocus = grid.containsFocus(); // checks document.activeElement const nodeInGrid = grid.containsFocus(someNode); // checks specific node ``` :::note **Angular users:** `BaseOverlayEditor` automatically registers/unregisters its panel — no manual calls needed. ::: ### Focus & Navigation API Programmatic cell focus and scroll-to-row: ```typescript // Focus a cell by column index or field name grid.focusCell(0, 2); // row 0, column index 2 grid.focusCell(5, 'email'); // row 5, field 'email' // Read the currently focused cell const cell = grid.focusedCell; if (cell) { console.log(`Row ${cell.rowIndex}, field "${cell.field}"`); } // Scroll a row into view (no-op if already visible) grid.scrollToRow(42); // Center the row with smooth scrolling grid.scrollToRow(42, { align: 'center', behavior: 'smooth' }); // Scroll by row ID (requires getRowId or id/_id property) grid.scrollToRowById('emp-42', { align: 'center' }); ``` **`ScrollToRowOptions`:** | Option | Type | Default | Description | | ---------- | --------------------------------------------- | ----------- | -------------------------------- | | `align` | `'start' \| 'center' \| 'end' \| 'nearest'` | `'nearest'` | Where to position the row | | `behavior` | `'instant' \| 'smooth'` | `'instant'` | Scroll animation | ## Events The grid dispatches standard `CustomEvent`s that bubble up the DOM. All events use `bubbles: true` and `composed: true`. ```ts // CoreEventsDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; const container = document.getElementById('demo-core-events')?.closest('.grid-demo'); if (container) { const grid = queryGrid('tbw-grid', container)!; const logEl = container.querySelector<HTMLElement>('[data-event-log]')!; let count = 0; grid.columns = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 150 }, { field: 'role', header: 'Role', width: 130 }, { field: 'score', header: 'Score', type: 'number', width: 80 }, ]; grid.sortable = true; grid.resizable = true; grid.rows = [ { id: 1, name: 'Alice', role: 'Engineer', score: 92 }, { id: 2, name: 'Bob', role: 'Designer', score: 78 }, { id: 3, name: 'Carol', role: 'Manager', score: 85 }, { id: 4, name: 'Dan', role: 'Engineer', score: 64 }, { id: 5, name: 'Eve', role: 'Designer', score: 91 }, ]; function logEvent(name: string, detail: unknown) { count++; const entry = document.createElement('div'); entry.style.cssText = 'padding:2px 0;border-bottom:1px solid var(--sl-color-gray-5);'; const summary = typeof detail === 'object' && detail ? JSON.stringify(detail).slice(0, 120) : String(detail); entry.innerHTML = `<span style="color:var(--sl-color-text-accent)">${count}.</span> <strong>${name}</strong> — ${summary}`; logEl.appendChild(entry); logEl.scrollTop = logEl.scrollHeight; } const events = ['cell-click', 'row-click', 'cell-activate', 'sort-change', 'column-resize', 'column-state-change', 'data-change']; for (const name of events) { grid.on(name, (detail: unknown) => logEvent(name, detail)); } container.querySelector('[data-clear-log]')!.addEventListener('click', () => { logEl.innerHTML = ''; count = 0; }); } ``` ### Listening to Events Cancelable events honour `preventDefault()` to reject the action — see the [Cancelable Events table](#cancelable-events) below. #### TypeScript ```typescript import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid<Employee>('#my-grid'); // Type-safe event listening grid.on('cell-click', ({ row, field }) => { console.log(row, field); }); // Cancelable events — call preventDefault() to reject the action grid.on('cell-commit', (detail, e) => { if (!isValid(detail.value)) { e.preventDefault(); // Rejects the edit } }); ``` #### React ```tsx import { DataGrid } from '@toolbox-web/grid-react'; import type { CellClickDetail } from '@toolbox-web/grid'; import type { CellCommitDetail } from '@toolbox-web/grid/plugins/editing'; function EmployeeGrid() { // Event props deliver the unwrapped detail directly const handleCellClick = useCallback((detail: CellClickDetail<Employee>) => { console.log(detail.row, detail.field); }, []); const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>, e?: CustomEvent) => { if (!isValid(detail.value)) { e?.preventDefault(); // Rejects the edit } }, []); return ( <DataGrid rows={employees} onCellClick={handleCellClick} onCellCommit={handleCellCommit} /> ); } ``` #### Vue ```html <script setup lang="ts"> import { TbwGrid } from '@toolbox-web/grid-vue'; import type { CellClickDetail } from '@toolbox-web/grid'; import type { CellCommitDetail } from '@toolbox-web/grid/plugins/editing'; function onCellClick(event: CustomEvent<CellClickDetail>) { console.log(event.detail.row, event.detail.field); } function onCellCommit(event: CustomEvent<CellCommitDetail>) { if (!isValid(event.detail.value)) { event.preventDefault(); // Rejects the edit } } </script> <template> <TbwGrid :rows="employees" @cell-click="onCellClick" @cell-commit="onCellCommit" /> </template> ``` #### Angular ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { CellClickDetail } from '@toolbox-web/grid'; import type { CellCommitDetail } from '@toolbox-web/grid/plugins/editing'; @Component({ imports: [Grid], template: ` <tbw-grid [rows]="employees" (cellClick)="onCellClick($event)" (cellCommit)="onCellCommit($event)" /> `, }) export class EmployeeGridComponent { onCellClick(detail: CellClickDetail<Employee>) { console.log(detail.row, detail.field); } onCellCommit(detail: CellCommitDetail<Employee>) { if (!isValid(detail.value)) { // To cancel, use the native event via kebab-case binding: // (cell-commit)="onCellCommit($event)" → event.preventDefault() } } } ``` :::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`. ::: ### Cancelable Events Five events support `preventDefault()` to reject the action: | Event | Plugin | What Canceling Does | |-------|--------|-------------------| | `cell-activate` | Core | Suppresses the default activation — prevents plugins (e.g. inline editing) from acting on the click/keypress | | `cell-commit` | Editing | Reverts the cell to its previous value | | `row-commit` | Editing | Reverts the entire row to pre-edit state | | `column-move` | Reorder | Rejects the column drag-and-drop | | `row-move` | Row Reorder | Rejects the row drag/keyboard move | `cell-activate` fires **first**, before any plugin handles the interaction, and for **both** pointer clicks and keyboard activation (Enter) — its `detail.trigger` is `'pointer'` or `'keyboard'`. Because it covers the keyboard path and is cancelable, prefer it over `cell-click` (pointer-only) for click-to-open / activate-to-edit patterns that must stay keyboard-accessible. ### Scroll & Render Recipes The `tbw-scroll` event fires (rAF-batched) on every viewport scroll; the `render` event fires once per render flush after all plugin `afterRender` hooks complete. Cookbook patterns — infinite scroll, sticky progress indicators, lazy cell hydration, post-`addRow` focus, phase-gated render handlers — live in [Recipes: Scroll & render driven UI](https://toolboxjs.com/grid/recipes/scroll-and-render.md). For the typed event payloads, see [`DataGridEventMap`](https://toolboxjs.com/grid/api/core/interfaces/datagrideventmap.md). ### Event Reference The [`DataGridEventMap`](https://toolboxjs.com/grid/api/core/interfaces/datagrideventmap.md) is the complete, auto-generated reference for every event the grid dispatches — core events and plugin events alike. It includes detail payload types, descriptions, and code examples, grouped by plugin. - [DataGridEventMap Reference](https://toolboxjs.com/grid/api/core/interfaces/datagrideventmap.md): Full list of every event, grouped by plugin, with payload types and code examples. :::note[Multi-Sort overrides `sort-change`] When `MultiSortPlugin` is active, the `sort-change` event payload changes from `SortChangeDetail` (single `{ field, direction }`) to `{ sortModel: SortModel[] }` (array of sort columns). This override cannot be expressed in `DataGridEventMap` because TypeScript declaration merging doesn't allow changing a property type. Import `SortModel` from `@toolbox-web/grid/plugins/multi-sort`. ::: ## CSS Custom Properties The grid exposes ~80 CSS custom properties for theming. See the [Theming guide](https://toolboxjs.com/grid/guides/theming.md#css-variable-reference) for the complete, categorised list and the [`GridIcons` interface](https://toolboxjs.com/grid/api/core/interfaces/gridicons.md) for the icon variables. ## Configuration Patterns The patterns that show how to *use* the configuration API live with their respective guides — this section is just a directory: | Pattern | Where it lives | |---|---| | Dynamic `rowClass` / `cellClass` | [Core → Row Styling](https://toolboxjs.com/grid/core.md#row-styling) & [Cell Styling](https://toolboxjs.com/grid/core.md#cell-styling) | | Type-level defaults (formatters, renderers, editors) | [Core → Type-Level Defaults](https://toolboxjs.com/grid/core.md#type-level-defaults) | | Row animation (expand/collapse, reorder, insert/remove) | [Core → Row Animation](https://toolboxjs.com/grid/core.md#row-animation) and the [`AnimationConfig` interface](https://toolboxjs.com/grid/api/core/interfaces/animationconfig.md) | | Shell layout (header, toolbar, tool panels) | [Shell plugin](https://toolboxjs.com/grid/plugins/shell.md) and the [`ShellConfig` interface](https://toolboxjs.com/grid/plugins/shell/interfaces/shellconfig.md) | | Icon customization (CSS + JS paths) | [Theming → Icon Customization](https://toolboxjs.com/grid/guides/theming.md#icon-customization) and the [`GridIcons` interface](https://toolboxjs.com/grid/api/core/interfaces/gridicons.md) | ## Accessibility & Keyboard The grid implements the [W3C WAI-ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) out of the box — ARIA roles (`grid` / `row` / `gridcell` / `columnheader`), `aria-rowindex` / `aria-colindex` / `aria-rowcount` / `aria-colcount`, `aria-sort`, `aria-selected`, `aria-readonly`, and `aria-checked` are applied automatically. Configure accessible naming via `gridAriaLabel` / `gridAriaDescribedBy` on [`GridConfig`](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md). Customise screen-reader announcements via [`A11yConfig`](https://toolboxjs.com/grid/api/core/interfaces/a11yconfig.md). For the full keyboard shortcut matrix, ARIA reference, screen-reader guidance, focus management, and high-contrast support, see the [Accessibility guide](https://toolboxjs.com/grid/guides/accessibility.md). ## Browser Support Modern evergreen browsers — **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. No polyfills or build-time transpilation of CSS are required, but the baseline is set by the CSS features the default theme relies on (notably `light-dark()`). Popover, CSS anchor positioning, and View Transitions are progressively enhanced and fall back automatically when unsupported. See the [Browser Support matrix](https://toolboxjs.com/grid/platform.md#browser-support) for the per-feature breakdown, including CSP, Trusted Types, and SSR. ## Related Documentation - [Core Features](https://toolboxjs.com/grid/core.md) — Detailed guide on columns, renderers, and formatters - [Shell plugin](https://toolboxjs.com/grid/plugins/shell.md) — Header bar, toolbar, and tool-panel sidebar - [Theming](https://toolboxjs.com/grid/guides/theming.md) — Complete CSS custom property reference and theme customization - [Getting Started](https://toolboxjs.com/grid/getting-started.md) — Installation and quick start guide - [Demos](https://toolboxjs.com/grid/demos.md) — Full-featured demo applications - [Angular Adapter](https://toolboxjs.com/grid/angular/getting-started.md) — `@toolbox-web/grid-angular` - [React Adapter](https://toolboxjs.com/grid/react/getting-started.md) — `@toolbox-web/grid-react` - [Vue Adapter](https://toolboxjs.com/grid/vue/getting-started.md) — `@toolbox-web/grid-vue` --- # Error & Warning Reference > Complete reference for all diagnostic codes emitted by @toolbox-web/grid. Each code links to an explanation and resolution steps. Every warning and error in `@toolbox-web/grid` includes a **diagnostic code** (e.g. `TBW001`) and a link back to this page. Find your code below for an explanation and fix. --- ## Configuration Validation <div id="tbw001" /> ### TBW001 — Missing Plugin (Column Property) A column uses a property (like `editable`, `editor`, `pinned`, or `group`) that requires a plugin, but that plugin isn't loaded. **Fix:** Import and add the required plugin: ```ts // For editing import '@toolbox-web/grid/features/editing'; grid.gridConfig = { features: { editing: true }, columns: [{ field: 'name', editable: true }], }; ``` Or with the plugin API directly: ```ts import { EditingPlugin } from '@toolbox-web/grid/plugins/editing'; grid.gridConfig = { plugins: [new EditingPlugin()], columns: [{ field: 'name', editable: true }], }; ``` - [Editing Plugin](https://toolboxjs.com/grid/plugins/editing.md) - [Pinned Columns Plugin](https://toolboxjs.com/grid/plugins/pinned-columns.md) - [Grouping Columns Plugin](https://toolboxjs.com/grid/plugins/grouping-columns.md) - [Getting Started](https://toolboxjs.com/grid/getting-started.md) <div id="tbw002" /> ### TBW002 — Missing Plugin (Config Property) A grid config property (like `columnGroups`) requires a plugin that isn't loaded. **Fix:** Same as TBW001 — import the required feature or plugin. - [Plugins Overview](https://toolboxjs.com/grid/plugins.md) <div id="tbw003" /> ### TBW003 — Config Rule Error A plugin's configuration rule detected an invalid combination. The error message describes the specific rule that was violated. **Fix:** Check the error message for the specific plugin and configuration that triggered the rule. Adjust the plugin config to avoid the invalid combination. <div id="tbw004" /> ### TBW004 — Config Rule Warning A plugin detected a potentially problematic configuration. This is a development-only warning — the grid will still work, but behavior may be unexpected. **Fix:** Read the warning message and adjust your config accordingly. - [Plugins Overview](https://toolboxjs.com/grid/plugins.md) --- ## Plugin Lifecycle <div id="tbw020" /> ### TBW020 — Missing Plugin Dependency A plugin requires another plugin to be loaded first. For example, `UndoRedoPlugin` requires `EditingPlugin`. **Fix:** Add the dependency plugin **before** the dependent one: ```ts import '@toolbox-web/grid/features/editing'; import '@toolbox-web/grid/features/undoRedo'; grid.gridConfig = { features: { editing: true, undoRedo: true, }, }; ``` - [Plugins Overview](https://toolboxjs.com/grid/plugins.md) - [Custom Plugins Guide](https://toolboxjs.com/grid/plugin-development/custom-plugins.md) <div id="tbw021" /> ### TBW021 — Optional Dependency Missing A plugin has an optional dependency that isn't loaded. The plugin will still work, but some features may be unavailable. This message is logged at the `debug` level and only visible when the browser DevTools "Verbose" log level is enabled. **Fix:** If you need the full feature set, add the optional dependency. Otherwise, this message can be safely ignored. <div id="tbw022" /> ### TBW022 — Incompatible Plugins Two plugins that are known to conflict are both loaded. Development-only warning. **Fix:** Remove one of the conflicting plugins. The warning message names both plugins and explains the conflict. :::note[PinnedColumnsPlugin + GroupingColumnsPlugin] As of the latest release, `PinnedColumnsPlugin` and `GroupingColumnsPlugin` **are compatible**. Pinning a grouped column temporarily moves it to the grid edge, causing the column group to fragment. Unpinning restores the column to its original position within the group. No action is needed. ::: <div id="tbw023" /> ### TBW023 — Plugin Alias Collapse Two entries in `plugins` resolve to the same canonical plugin — for example the same plugin class instantiated twice, or one instance passed by class and another by one of its registered aliases (`ReorderPlugin` also answers to `reorder`). Only one instance is kept; the others' configs are merged into it. **Fix:** Pass the plugin once, with a single config object. <div id="tbw024" /> ### TBW024 — Plugin Event Handler Error An error was thrown inside a plugin event handler. This doesn't crash the grid — the error is caught and logged — but it indicates a bug in plugin code. **Fix:** Check the stack trace to find and fix the error in the plugin's event handler. - [Custom Plugins Guide](https://toolboxjs.com/grid/plugin-development/custom-plugins.md) <div id="tbw025" /> ### TBW025 — Plugin Alias Config Conflict Two plugin instances resolve to the same canonical name (for example, the same plugin class instantiated twice), and they supply conflicting values for the same configuration key. The grid cannot decide which one wins. **Fix:** Pass the option on a single instance, and remove the duplicate plugin. ```ts // ❌ Conflict: both instances configure `animation` plugins: [ new RowDragDropPlugin({ animation: 'fade' }), new RowDragDropPlugin({ animation: 'flip' }), ] // ✅ Configure on one instance only plugins: [ new RowDragDropPlugin({ animation: 'flip' }), ] ``` --- ## Feature Registry <div id="tbw030" /> ### TBW030 — Feature Re-registered A feature was registered more than once. The previous registration was overwritten. This typically happens when two different versions of the same feature module are loaded. **Fix:** Check your bundle for duplicate imports. Each feature should be imported exactly once. <div id="tbw031" /> ### TBW031 — Feature Not Imported A feature is configured in `gridConfig.features` but its module was never imported. Features require a side-effect import to register themselves. **Fix:** Add the import shown in the warning: ```ts import '@toolbox-web/grid/features/selection'; grid.gridConfig = { features: { selection: 'row' }, }; ``` Framework adapters handle this automatically — if you're using Angular, React, or Vue adapters, import from the adapter package instead. - [Getting Started](https://toolboxjs.com/grid/getting-started.md) <div id="tbw032" /> ### TBW032 — Feature Missing Dependency A feature requires another feature to be enabled. For example, `clipboard` requires `selection`. **Fix:** Add the required feature to your config: ```ts grid.gridConfig = { features: { selection: 'range', clipboard: true, }, }; ``` - [Getting Started](https://toolboxjs.com/grid/getting-started.md) --- ## Row Operations <div id="tbw040" /> ### TBW040 — Missing Row ID The grid needs to identify a row but couldn't determine its ID. This happens when: - Rows don't have an `id` or `_id` property - No `getRowId` function is configured **Fix:** Either add an `id` field to your data, or configure `getRowId`: ```ts grid.gridConfig = { getRowId: (row) => row.employeeNumber, }; ``` - [Core Features](https://toolboxjs.com/grid/core.md) <div id="tbw041" /> ### TBW041 — Row Not Found A row update operation (`updateRow` / `updateRows`) referenced a row ID that doesn't exist in the grid's dataset. Rows that are merely filtered or paged **out of view** are still updatable — the grid resolves the ID against the full dataset, not just the visible rows. This code is only emitted (as a **warning**, never a thrown error) when the ID matches no row at all; the offending update is skipped so the rest of the batch and the grid keep working. **Fix:** Verify the row ID is correct and the row hasn't been removed. Validate the ID against your backing `rows` data (that's the full dataset the grid resolves against). Note that `grid.getRow(id)` is **view-scoped** — it returns `undefined` for rows that are merely filtered or paged out of view, so it is not a reliable "does this row exist in the dataset?" check. - [Core Features](https://toolboxjs.com/grid/core.md) --- ## Column Operations <div id="tbw050" /> ### TBW050 — Invalid Column Width A column's `width` value is not a valid CSS grid track size. Development-only warning. **Fix:** Use a number (interpreted as pixels) or a valid CSS string: ```ts { field: 'name', width: 200 } // 200px { field: 'name', width: '30%' } // 30% { field: 'name', width: '2fr' } // 2 fractional units { field: 'name', width: 'auto' } // auto ``` - [Core Features — Columns](https://toolboxjs.com/grid/core.md) --- ## Rendering Callbacks <div id="tbw060" /> ### TBW060 — rowClass Callback Error The `rowClass` function threw an error during rendering. The row will render without dynamic classes. **Fix:** Check your `rowClass` function for null/undefined access or type errors. <div id="tbw061" /> ### TBW061 — cellClass Callback Error A column's `cellClass` function threw an error. The cell will render without dynamic classes. **Fix:** Check the `cellClass` function on the column named in the warning. <div id="tbw062" /> ### TBW062 — Format Error A column's `format` function threw an error. The raw value will be displayed instead. **Fix:** Check the `format` function on the named column. Ensure it handles null/undefined values gracefully. <div id="tbw063" /> ### TBW063 — External View Mount Error An `externalView.mount()` callback threw during cell rendering. **Fix:** Debug the `mount` function in your external view configuration for the named column. <div id="tbw064" /> ### TBW064 — External View Dispatch Error The `mount-external-view` event dispatch failed for a cell. **Fix:** Check event listeners attached to the `mount-external-view` event on the grid. - [Core Features — Rendering](https://toolboxjs.com/grid/core.md) --- ## Shell <div id="tbw070" /> ### TBW070 — Tool Panel Missing Attributes A `<tbw-grid-tool-panel>` light DOM element is missing required `id` or `title` attributes. **Fix:** Add both attributes: ```html <tbw-grid-tool-panel id="my-panel" title="My Panel"> Panel content here </tbw-grid-tool-panel> ``` <div id="tbw071" /> ### TBW071 — No Tool Panels `openToolPanel()` was called but no tool panels are registered. **Fix:** Register at least one tool panel before attempting to open the panel. <div id="tbw072" /> ### TBW072 — Tool Panel Not Found `toggleToolPanelSection()` was called with a section ID that doesn't match any registered panel. **Fix:** Verify the section ID matches a registered tool panel's `id`. <div id="tbw073" /> ### TBW073 — Duplicate Tool Panel A tool panel with the same `id` was registered twice. The duplicate was ignored. **Fix:** Ensure each tool panel has a unique `id`. <div id="tbw074" /> ### TBW074 — Duplicate Header Content Header content with the same `id` was registered twice. **Fix:** Use unique IDs for header content registrations. <div id="tbw075" /> ### TBW075 — Duplicate Toolbar Content Toolbar content with the same `id` was registered twice. **Fix:** Use unique IDs for toolbar content registrations. - [Architecture — Shell](https://toolboxjs.com/grid/architecture.md) --- ## Editing <div id="tbw080" /> ### TBW080 — Editor Mount Error An external editor's `mount()` function threw during edit activation. **Fix:** Debug the editor's `mount` function. Ensure it handles the editor context correctly. - [Editing Plugin](https://toolboxjs.com/grid/plugins/editing.md) --- ## Print <div id="tbw090" /> ### TBW090 — Print In Progress `print()` was called while a print operation is already running. **Fix:** Wait for the current print to complete before starting another. Listen for the `print-complete` event. <div id="tbw091" /> ### TBW091 — Grid Not Available `print()` was called but the grid element is not available (likely disconnected from the DOM). **Fix:** Ensure the grid is mounted before calling `print()`. <div id="tbw092" /> ### TBW092 — Print Failed The print operation encountered an error. The `print-complete` event will fire with `success: false`. **Fix:** Check the error details in the console. Common causes include the grid being too large or the user canceling the print dialog. <div id="tbw093" /> ### TBW093 — Duplicate Grid ID (Print) Multiple elements on the page share the same `id` as the grid. This interferes with print isolation. **Fix:** Assign a unique `id` to each grid element on the page. - [Print Plugin](https://toolboxjs.com/grid/plugins/print.md) --- ## Clipboard <div id="tbw100" /> ### TBW100 — Clipboard API Failed The browser's `navigator.clipboard.writeText()` failed. The grid falls back to `document.execCommand('copy')`. **Fix:** This usually happens when the page isn't focused or lacks the clipboard permission. The fallback typically succeeds, so this warning is informational. - [Clipboard Plugin](https://toolboxjs.com/grid/plugins/clipboard.md) --- ## Plugin-Specific <div id="tbw110" /> ### TBW110 — Missing Breakpoint (Responsive) `ResponsivePlugin` is loaded but no `breakpoint` is configured. The plugin will not activate. **Fix:** Set a breakpoint based on your container width: ```ts grid.gridConfig = { features: { responsive: { breakpoint: 600 }, }, }; ``` - [Responsive Plugin](https://toolboxjs.com/grid/plugins/responsive.md) <div id="tbw111" /> ### TBW111 — Transaction In Progress (Undo/Redo) `beginTransaction()` was called while a transaction is already open. **Fix:** Call `endTransaction()` before starting a new transaction. <div id="tbw112" /> ### TBW112 — No Transaction (Undo/Redo) `endTransaction()` was called without a matching `beginTransaction()`. **Fix:** Ensure every `endTransaction()` has a preceding `beginTransaction()`. - [Undo/Redo Plugin](https://toolboxjs.com/grid/plugins/undo-redo.md) <div id="tbw113" /> ### TBW113 — Column Group Missing ID A `ColumnGroupDefinition` has neither `id` nor `header`. The grid cannot generate an identifier. **Fix:** Add either an `id` or `header` to every column group definition: ```ts columnGroups: [ { id: 'personal', header: 'Personal Info', children: ['name', 'age'] }, ] ``` <div id="tbw114" /> ### TBW114 — Conflicting Column Groups `columnGroups` are defined in both `gridConfig` and the `groupingColumns` feature config. The feature config takes precedence. **Fix:** Define column groups in only one place — preferably in the feature config. - [Grouping Columns Plugin](https://toolboxjs.com/grid/plugins/grouping-columns.md) --- ## Style Injection <div id="tbw120" /> ### TBW120 — Style Extraction Failed The grid couldn't extract its CSS from `document.styleSheets` due to a CORS or access error. **Fix:** Ensure the grid's CSS file is served from the same origin, or add CORS headers to the stylesheet response. <div id="tbw121" /> ### TBW121 — Stylesheet Not Found The grid's CSS was not found in `document.styleSheets`. The grid will render without styling. **Fix:** Ensure you're importing the grid's CSS. For most setups: ```ts import '@toolbox-web/grid/style.css'; ``` Or link it in your HTML: ```html <link rel="stylesheet" href="node_modules/@toolbox-web/grid/style.css" /> ``` - [Getting Started — Installation](https://toolboxjs.com/grid/getting-started.md) - [Theming Guide](https://toolboxjs.com/grid/guides/theming.md) --- ## Attribute Parsing <div id="tbw130" /> ### TBW130 — Invalid Attribute JSON An HTML attribute (`rows`, `columns`, or `grid-config`) contains invalid JSON. **Fix:** Verify the attribute value is valid JSON. For complex config, use JavaScript properties instead of HTML attributes: ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [...], ... }; ``` - [Getting Started](https://toolboxjs.com/grid/getting-started.md) --- ## DataSource <div id="tbw140" /> ### TBW140 — DataSource Fetch Error The `getRows()` call on the data source threw an error or returned a rejected promise. **Fix:** Check your data source implementation for network errors, malformed responses, or server-side failures. The error detail is included in the diagnostic message and in the `datasource:error` event. <div id="tbw141" /> ### TBW141 — DataSource Child Fetch Error The `getChildRows()` call on the data source threw an error or returned a rejected promise. **Fix:** Check your `getChildRows()` implementation. The error and parent context are included in the diagnostic message. <div id="tbw142" /> ### TBW142 — No Child Row Handler A plugin requested child rows via `datasource:fetch-children`, but the active data source does not implement `getChildRows()`. **Fix:** Add a `getChildRows()` method to your `ServerSideDataSource` if you are using Tree, GroupingRows, or MasterDetail plugins with server-side data. <div id="tbw143" /> ### TBW143 — DataSource Request Throttled A data fetch request was skipped because the maximum number of concurrent requests was already in-flight. **Info:** This is a debug-level diagnostic. The request will be retried automatically on the next scroll or render cycle. --- # Platform & Support > What you can rely on when you depend on @toolbox-web/grid — adapter support matrix, browser baseline, CSP & security handling, server-side rendering, deprecation policy, and release cadence. Everything you need to know about depending on `@toolbox-web/grid`: which **environments** it runs in (browsers, frameworks, SSR), how it behaves under a strict Content Security Policy, and — in the second half — the **commitments** that govern what breaks, when, and how you find out. The grid is **feature-complete**. There is no public roadmap, because it already does what it set out to do — and "we plan to add X" is not something you should have to bet a production dependency on. What you _should_ be able to bet on is this page. Every statement below is checkable against the repository: the version and peer-range tables are generated at docs-build time from the packages' `package.json` files, and the policies describe what the project has actually done across v1, v2, and v3. ## Support at a glance | | | | --- | --- | | **Current core version** | `@toolbox-web/grid` v3.8.0 | | **Current adapter versions** | `grid-angular` v2.6.0 · `grid-react` v2.6.0 · `grid-vue` v2.6.0 | | **Runtime dependencies (core)** | 0 | | **License** | MIT | | **Versioning** | Semantic Versioning, per package | | **Release automation** | [release-please](https://github.com/googleapis/release-please) from Conventional Commits | | **Distribution** | npm, `latest` dist-tag (prereleases go to `next`) | ## Framework Support The adapters are thin wrappers around the same web component. Their supported framework versions are whatever their published `peerDependencies` say — which is what npm, pnpm, and bun actually enforce at install time: | Package | Version | Framework peer | Requires | | ------- | ------- | -------------- | -------- | | `@toolbox-web/grid-angular` | 2.6.0 | `@angular/core` >=17.0.0 | `@toolbox-web/grid` ^3.0.0 | | `@toolbox-web/grid-react` | 2.6.0 | `react` and `react-dom` >=18.0.0 | `@toolbox-web/grid` ^3.0.0 | | `@toolbox-web/grid-vue` | 2.6.0 | `vue` >=3.3.0 | `@toolbox-web/grid` ^3.0.0 | The ranges are **open-ended on the upper bound** (`>=`) on purpose: a new framework major does not, by itself, invalidate the adapter. If one genuinely breaks it, that is a bug fix, not a support drop. ### When a framework version is dropped - A framework major is supported **for as long as its own vendor supports it**. The adapters do not drop a version the framework team still maintains. - Raising a `peerDependencies` floor is a **breaking change**, so it only happens in an adapter **major** release, documented in that adapter's changelog. - **You do not need an adapter.** `<tbw-grid>` is a standard custom element; if an adapter ever stops covering your framework version, the core package keeps working — you lose template-level ergonomics, not functionality. See [Framework Integration](https://toolboxjs.com/grid/framework-adapters.md) for what the adapters add on top. - **Older adapter lines are not listed above.** This page always describes the current release. If you are pinned to an older framework major, the previous adapter major still has whatever range it published — ask npm rather than trusting a table: ```sh npm view @toolbox-web/grid-angular@2 peerDependencies ``` :::tip[Two grid versions on one page] Micro-frontend setups can run two different grid majors side by side without a conflict — see [Multi-version coexistence](https://toolboxjs.com/grid/guides/multi-version.md). That makes an incremental upgrade possible without a big-bang migration across every team. ::: ## Browser Support `@toolbox-web/grid` targets **modern evergreen browsers**. There is no transpilation to ES5 and no polyfill bundle — the grid ships the same standards-based code it was written in. | Browser | Minimum version | Released | | -------------- | --------------- | ----------- | | Chrome / Edge | **123** | March 2024 | | Firefox | **121** | Dec 2023 | | Safari (macOS / iOS) | **17.5** | May 2024 | Older versions may work for basic rendering but are **not tested and not supported**. Raising this baseline is treated as a **breaking change** and lands only in a major. ### What sets the floor The minimums above are driven by the platform features the grid depends on. Each row is a hard requirement unless marked as progressively enhanced. | Platform feature | Used for | Chrome/Edge | Firefox | Safari | | ---------------- | -------- | ----------- | ------- | ------ | | [`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark) | Every themeable color token resolves light/dark in one declaration | 123 | 120 | 17.5 | | [`:has()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) | Row/cell state styling, print isolation | 105 | 121 | 15.4 | | [CSS Nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting) | Authored style sheets | 120 | 117 | 17.2 | | [`@layer`](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) | `tbw-base` → `tbw-plugins` → `tbw-theme` cascade order | 99 | 97 | 15.4 | | [`adoptedStyleSheets`](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets) | Style injection that survives DOM rebuilds | 73 | 101 | 16.4 | | [Custom Elements v1](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) | `<tbw-grid>` itself | 67 | 63 | 10.1 | | `ResizeObserver` / `IntersectionObserver` | Virtualization, column fitting, responsive layout | 64 | 69 | 13.1 | | [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) | Tooltips, dropdown tool panel — **progressively enhanced** | 114 | 125 | 17 | | [CSS anchor positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) | Tooltip placement — **progressively enhanced** | 125 | — | — | :::note[Why no `light-dark()` fallback?] The theme system defines each color once and lets the browser pick the light or dark value. Emitting a `prefers-color-scheme` fallback for every token would roughly double the size of the shipped CSS and reintroduce the flash-of-wrong-theme the token system exists to avoid. ::: ### Progressive enhancement Two features degrade rather than break when the platform lacks support: - **Tooltip and dropdown tool panel** — the [Tooltip plugin](https://toolboxjs.com/grid/plugins/tooltip.md) checks `HTMLElement.prototype.showPopover` before using the top layer, and checks `CSS.supports('anchor-name', '--x')` before using anchor positioning. Where either is missing it falls back to absolute positioning computed from bounding rects. - **Print isolation** — the [Print plugin](https://toolboxjs.com/grid/plugins/print.md) uses `:has()` to hide everything outside the grid. This is a hard requirement; the plugin has no fallback. ### Testing matrix The e2e suite runs against Chromium, Firefox, and WebKit via Playwright. Unit tests run in [happy-dom](https://github.com/capricorn86/happy-dom), which is a DOM emulation and **not** a browser — behaviour differences (layout, `getBoundingClientRect`, CSS cascade) are covered by e2e, not unit tests. - [Automated testing](https://toolboxjs.com/grid/guides/automated-testing.md): Selectors and helpers for testing the grid --- ## Security The grid is a rendering engine for **your** data. It never fetches, never evaluates, and never persists anything on its own — but the extension points it gives you can introduce vulnerabilities if used carelessly. Its strongest security property is structural: the core declares **0 runtime dependencies**, and so do all three adapters. There is no transitive dependency tree to audit, and no supply-chain surface beyond the package itself. ### Reporting a vulnerability | | | | --- | --- | | **Report privately** | [GitHub Security Advisories](https://github.com/OysteinAmundsen/toolbox/security/advisories/new) | | **Report publicly** | [A new issue](https://github.com/OysteinAmundsen/toolbox/issues/new) with the `security` label | | **Policy** | [`SECURITY.md`](https://github.com/OysteinAmundsen/toolbox/blob/main/SECURITY.md) | | **Advisories published to** | The repository [Security tab](https://github.com/OysteinAmundsen/toolbox/security/advisories) and, from there, the GitHub Advisory Database | | **Supported for fixes** | The current major only — fixes ship as a new patch release | | **Repository controls** | GitHub secret scanning and push protection are enabled | :::caution[Response time, honestly] This is a solo-maintained open-source project. There is no contractual response SLA: critical vulnerabilities are prioritised, everything else is handled as time allows. `SECURITY.md` says the same thing — this page does not upgrade that promise. ::: ### What the grid sanitizes for you Every path where the grid turns a **string you returned** into markup runs through an internal sanitizer before it reaches `innerHTML`. The sanitizer strips dangerous tags (`script`, `iframe`, `object`, `embed`, `form`, `style`, `link`, `meta`, `base`, and friends), all `on*` event-handler attributes, and `javascript:` / `vbscript:` / `data:` / `blob:` URLs from `href`, `src`, `srcdoc`, `formaction`, `poster`, and `srcset`. This covers string-returning cell renderers, group-header renderers, `gridConfig.icons.*` (icon strings are usually inline SVG), and light-DOM tool panel fallback content. ### What you must sanitize yourself Renderers that receive a DOM element and write to it directly **bypass** that sanitizer. Writing user-controlled data to `innerHTML` yourself is the single most likely way to introduce XSS. ```ts // ❌ XSS: `row.notes` may contain <img src=x onerror=...> renderer: (cell, { row }) => { cell.innerHTML = row.notes; } // ✅ Text is escaped by the DOM renderer: (cell, { row }) => { cell.textContent = row.notes; } // ✅ Structured markup, still safe renderer: (cell, { row }) => { const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = row.status; cell.replaceChildren(badge); } ``` If you genuinely need rich HTML from an untrusted source, sanitize it with a hardened library such as [DOMPurify](https://github.com/cure53/DOMPurify) — do not hand-roll an escaper. The grid's built-in sanitizer is **defense in depth for its own render paths**, not a general-purpose sanitization service; it is not exported. - [Renderer security](https://toolboxjs.com/grid/core.md#renderer-security-avoid-innerhtml): The full rule in the core configuration guide ### Content Security Policy The grid injects its styles through [`document.adoptedStyleSheets`](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets), built from `CSSStyleSheet` objects. Constructable style sheets are **not** governed by `style-src`, so a strict policy works without `'unsafe-inline'`: ``` Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; ``` Notes: - **No `'unsafe-eval'` is required.** The grid never calls `eval`, `new Function`, or `setTimeout` with a string. `{{ }}` template expressions are evaluated by a purpose-built recursive-descent parser over an allowlisted grammar, not by dynamic code generation. - **`img-src`** must allow whatever your renderers load. The grid's own icons are inline SVG markup, not external images. - **Inline `style` attributes** are set imperatively (`element.style.width = …`) for column widths and virtualization offsets. These are DOM property writes, not CSS text, so they are not blocked by `style-src`. You do **not** need `style-src-attr 'unsafe-inline'`. - If styles silently fail to apply, see [Styles not applying (CSP)](https://toolboxjs.com/grid/guides/troubleshooting.md#styles-not-applying-csp). ### Trusted Types `require-trusted-types-for 'script'` is **not** supported out of the box. The grid's sanitizer parses markup by assigning to a detached `<template>` element's `innerHTML`, which is a Trusted Types sink. If you enforce Trusted Types, install a default policy that runs your own sanitizer: ```ts if (window.trustedTypes?.createPolicy) { window.trustedTypes.createPolicy('default', { createHTML: (input) => DOMPurify.sanitize(input), }); } ``` :::note[What the built-in sanitizer already does] The policy above is required only because the parse step itself is a Trusted Types sink — it is not a sign that the grid hands raw markup to the DOM. Every string returned by a `renderer`, `headerRenderer` or template is first passed through the grid's sanitizer, which: - drops non-allow-listed elements (`<script>`, `<iframe>`, `<object>`, `<embed>`, `<link>`, `<meta>`, `<base>`, `<form>`); - strips every `on*` event-handler attribute; - strips the `is=` attribute, so markup cannot upgrade a plain tag into a registered customized built-in and smuggle behaviour past the tag allow-list; - rejects `javascript:` / `vbscript:` / `data:` URLs in `href`, `src`, `action` and friends; - rejects `expression()`, `javascript:` and `behavior:` inside inline `style`; - HTML-escapes every `{{ }}` interpolation in template strings, so row data can never contribute markup or break out of an attribute. Your default policy therefore runs **in addition to** those guarantees, not instead of them. ::: ### Handling untrusted data - **Validate before assigning to `grid.rows`.** The grid trusts the shape you give it. Rows with duplicate or missing IDs cause incorrect selection and edit tracking, not an error. - **Clipboard and paste.** The [Clipboard plugin](https://toolboxjs.com/grid/plugins/clipboard.md) writes values into your row objects. Use the `onPaste` column hook to validate and coerce incoming values before they land in your model. - **Export.** The [Export plugin](https://toolboxjs.com/grid/plugins/export.md) writes CSV and Excel XML. Values that start with `=`, `+`, `-`, `@`, a tab or a carriage return are prefixed with `'` so a spreadsheet treats them as text — this guards against [CSV injection](https://owasp.org/www-community/attacks/CSV_Injection) and is **on by default**. Only turn it off (`escapeFormulas: false`) when the exported data is fully trusted and the leading apostrophe is unacceptable. --- ## Server-Side Rendering `<tbw-grid>` is a custom element. Custom elements are a **browser** API — there is no DOM to upgrade on the server, so the grid renders no markup during SSR. :::caution[Do not import the grid on the server] Importing `@toolbox-web/grid` registers the custom element at module-evaluation time, which touches `customElements`. That global does not exist in Node, so a top-level server import throws. Always load the grid behind a client-only boundary — the recipes below show how for each framework. ::: Beyond that one rule, there is nothing to reconcile: - **Hydration is automatic.** On the client the element upgrades and renders as soon as it is connected. There is no hydration mismatch, because the server emitted no grid content. - **Feature side-effect imports follow the same rule.** `@toolbox-web/grid/features/selection` only writes a factory into a module-scoped map, but it pulls in the core module — so keep it inside the same client-only boundary as the grid itself. :::caution[`ssr` prop removed in v3] Both the React and Vue adapters used to accept an `ssr` prop (and export an `SSRProps` type). It was a no-op and was removed in v3 — see the [v3 migration guide](https://toolboxjs.com/grid/guides/migration-v3.md#3-ssrprops--ssr-prop-removed). ::: ### Framework recipes | Framework | Recommendation | | --------- | -------------- | | **Next.js (App Router)** | Mark the component `'use client'` **and** load it via `next/dynamic` with `{ ssr: false }` — `'use client'` alone still pre-renders on the server. | | **Remix / React Router** | Render inside a `ClientOnly` boundary, or gate on a `useEffect`-set `mounted` flag. | | **Nuxt** | Wrap the grid in `<ClientOnly>`. | | **Astro** | Use `client:only="react"` / `"vue"` (or `client:only` for the vanilla element). | | **Angular SSR** | Guard the import behind `isPlatformBrowser(...)`, or defer it with `@defer (on viewport)`. | Reserve the grid's height in your server-rendered markup (e.g. a wrapper with a fixed `height`) so hydration doesn't cause layout shift. --- ## Versioning & Deprecation Semantic Versioning is applied **per package**. `@toolbox-web/grid` and each adapter are independent release-please components, so a grid major does not force an adapter major unless the adapter's own API changes (its peer range widening usually does). ### What the JSDoc tags guarantee | Tag | Meaning | | --- | ------- | | `@since <version>` | The symbol has existed, with this behaviour, since that version. Rendered as a version pill in the [API reference](https://toolboxjs.com/grid/api-reference.md). | | `@deprecated` | Still works, still tested, still shipped. Scheduled for removal in a future major, with the replacement named in the same comment. | | `@internal` | Exported for advanced plugin authors, **not** covered by SemVer. May change in a minor. | ### The removal window **A public symbol is deprecated for at least one full major before it is removed.** That is the practice v1 → v2 → v3 actually followed: symbols marked `@deprecated` during the v1 line were removed in v2, and the v2 deprecations were removed in v3. Removals are listed in the migration guide for that major, with the replacement for each one. The v3 guide is the precedent to look at: - [Migrating to v3](https://toolboxjs.com/grid/guides/migration-v3.md): Every removal in v3, why it happened, and the exact replacement At the time of writing there are **no `@deprecated` symbols** anywhere in the shipped source of the grid or the three adapters — v3 cleared the backlog. Anything newly deprecated must name its removal major in the same comment. ### Maintenance branches When work on the next major starts, the previous major gets a long-lived `<major>.x` branch (`2.x` today) which keeps its own release stream, so an important fix can be cherry-picked back. There is **no formal LTS window**: the previous major is maintained on a best-effort basis, and staying on the current major is the supported path. --- ## Plugin API Compatibility Every one of the 24 shipped plugins is built on the **same public API you get**. There is no privileged internal channel, no "enterprise" hook set, and no plugin that reaches into something you cannot reach into. If a first-party plugin can do it, a third-party plugin can do it — which is also why the plugin API cannot quietly rot: breaking it would break the grid's own feature set first. ### What is covered by SemVer Everything exported from the package root `@toolbox-web/grid` and from `@toolbox-web/grid/plugins/*`: - `BaseGridPlugin` and its lifecycle (`init`, `afterRender`, `destroy`, …) - Hook payload types — `CellClickEvent`, `RowClickEvent`, `HeaderClickEvent`, `ScrollEvent`, `CellCoords`, `HookName` - Render-context types — `AfterCellRenderContext`, `AfterRowRenderContext`, `CollectHeaderRowsContext`, `HeaderRowCell`, `HeaderRowContribution` - The inter-plugin contract — `PluginManifest`, `PluginDependency`, `PluginQuery`, `QueryDefinition`, `EventDefinition` - DOM contracts — `GridClasses`, `GridSelectors`, `GridDataAttrs`, `GridCSSVars` ### What is not Types documented as `@internal` — `InternalGrid`, `ColumnInternal`, `VirtualState`, `CellContext`, `ResizeController`, and the underscore-prefixed members they expose — are exported so power users are not forced into `as any`, but they describe implementation details and **can change in a minor release**. A plugin built only on the list above will not be broken by a minor. ### What a major obliges When the grid takes a major, third-party plugin authors get: the removals enumerated in the migration guide, a deprecation cycle that preceded them, and a `peerDependencies` range to widen. Plugins that stayed on the public API surface typically need only the range bump. - [Writing a custom plugin](https://toolboxjs.com/grid/plugin-development/custom-plugins.md): The plugin lifecycle, hooks, manifest, and inter-plugin queries --- ## Release Cadence Releases are automated, not scheduled: every merge to `main` that carries a `feat`/`fix`/`perf` commit updates a release-please pull request, and merging that PR tags, publishes to npm, and deploys the docs. There is no release train to wait for. | | | | --- | --- | | **Patch** (`fix:`, `perf:`) | Bug fixes and optimisations. Always safe to take. | | **Minor** (`feat:`, `enhance:`) | Additive API. Existing code keeps compiling. | | **Major** (`!` / `BREAKING CHANGE:`) | Removals and behaviour changes, with a migration guide. | | **Prereleases** | Published under the `next` dist-tag, never `latest`. | For reference, the actual history: **over 120 stable `@toolbox-web/grid` releases** since 2026-01-21, with majors on 2026-01-21 (v1), 2026-04-16 (v2), and 2026-07-14 (v3) — roughly one major per quarter, each with a written migration guide. Every release runs the same gate: unit tests, lint, build, bundle-size budget, Playwright e2e across three browser engines, and a performance benchmark suite. A regression in any of them blocks the release. - [Changelog](https://toolboxjs.com/grid/changelog.md): Every release, generated from the commit history ### What this page does not promise Being explicit about the gaps is the point of the exercise: - **No contractual SLA.** Solo-maintained MIT software. Response times are best-effort. - **No LTS.** Only the current major receives fixes as a matter of policy. - **No paid support tier.** If continuity matters to you, the mitigations are the MIT licence, the zero-dependency footprint, and the fact that the whole thing is a standard custom element you can vendor. - **No feature roadmap.** New features arrive when someone needs them and opens an issue. :::note[Verified as of] The support and stability sections reflect `@toolbox-web/grid` v3.8.0 with the three adapters at v2.6.0. The version and peer-range tables are read from the packages at docs-build time, so they cannot drift — and where one cell stands for several packages (`react` with `react-dom`, the core range across all three adapters), the docs build **fails** if those ranges ever stop matching. ::: --- --- # 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 lets you add capabilities without growing the core bundle. 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 a 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 `<tbw-grid>`. Import the matching per-feature directive (e.g. [`GridSelectionDirective`](https://toolboxjs.com/grid/angular/api/directives/gridselectiondirective.md) from `@toolbox-web/grid-angular/features/selection`) alongside `Grid` in the component's `imports`. This keeps each feature tree-shakeable. Apps that configure plugins through `[gridConfig]="{ features: { … } }"` don't need the per-feature directives — see the [Angular getting-started guide](https://toolboxjs.com/grid/angular/getting-started.md#two-ways-to-configure-features) for the full breakdown. ::: ## Available Plugins ### Editing & Data Entry | Plugin | Description | | ------ | ----------- | | [Editing](https://toolboxjs.com/grid/plugins/editing.md) | Inline cell editing with built-in editors | | [Undo/Redo](https://toolboxjs.com/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](https://toolboxjs.com/grid/plugins/selection.md) | Cell, row, and range selection with keyboard support | | [Clipboard](https://toolboxjs.com/grid/plugins/clipboard.md) | Copy/paste with Ctrl+C/V | | [Context Menu](https://toolboxjs.com/grid/plugins/context-menu.md) | Right-click context menus with submenus | | [Reorder](https://toolboxjs.com/grid/plugins/reorder-columns.md) | Drag-and-drop column reordering | | [Row Drag & Drop](https://toolboxjs.com/grid/plugins/row-drag-drop.md) | Drag-and-drop row reordering and cross-grid row transfer, with keyboard support | ### Filtering & Sorting | Plugin | Description | | ------ | ----------- | | [Filtering](https://toolboxjs.com/grid/plugins/filtering.md) | Column header filters with search and dropdown | | [Multi-Sort](https://toolboxjs.com/grid/plugins/multi-sort.md) | Sort by multiple columns with priority indicators | ### Grouping & Hierarchy | Plugin | Description | | ------ | ----------- | | [Row Grouping](https://toolboxjs.com/grid/plugins/grouping-rows.md) | Group rows by field values with expand/collapse | | [Column Grouping](https://toolboxjs.com/grid/plugins/grouping-columns.md) | Visual column grouping with nested headers | | [Tree](https://toolboxjs.com/grid/plugins/tree.md) | Hierarchical tree data display | | [Master-Detail](https://toolboxjs.com/grid/plugins/master-detail.md) | Expandable detail rows | | [Pivot](https://toolboxjs.com/grid/plugins/pivot.md) | Pivot table transformation with aggregations | ### Layout & Display | Plugin | Description | | ------ | ----------- | | [Shell](https://toolboxjs.com/grid/plugins/shell.md) | Header bar (title + toolbar) and collapsible tool-panel sidebar | | [Pinned Columns](https://toolboxjs.com/grid/plugins/pinned-columns.md) | Pin columns to left or right edge | | [Pinned Rows](https://toolboxjs.com/grid/plugins/pinned-rows.md) | Status bar with aggregations and custom panels | | [Sticky Rows](https://toolboxjs.com/grid/plugins/sticky-rows.md) | Keep matching rows pinned in view while the rest of the grid scrolls | | [Visibility](https://toolboxjs.com/grid/plugins/visibility.md) | Show/hide columns dynamically | | [Column Virtualization](https://toolboxjs.com/grid/plugins/column-virtualization.md) | Performance optimization for many columns | | [Responsive](https://toolboxjs.com/grid/plugins/responsive.md) | Card layout for narrow containers and mobile | | [Tooltip](https://toolboxjs.com/grid/plugins/tooltip.md) | Popover tooltips for truncated header and cell text | ### Data & Export | Plugin | Description | | ------ | ----------- | | [Export](https://toolboxjs.com/grid/plugins/export.md) | Export to CSV, Excel (XML), and JSON formats | | [Print](https://toolboxjs.com/grid/plugins/print.md) | Print-optimized layout with styling | | [Server-Side](https://toolboxjs.com/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](https://toolboxjs.com/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 #### Vanilla ```typescript // 1. Import features (side-effect imports for tree-shaking) import '@toolbox-web/grid/features/selection'; import '@toolbox-web/grid/features/filtering'; import '@toolbox-web/grid/features/editing'; // 2. Configure declaratively grid.gridConfig = { columns: [...], features: { selection: 'row', // shorthand filtering: { debounceMs: 300 }, // full config object editing: 'dblclick', // shorthand }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/selection'; import '@toolbox-web/grid-react/features/filtering'; import '@toolbox-web/grid-react/features/editing'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns, features: { selection: 'row', // shorthand filtering: { debounceMs: 300 }, // full config object editing: 'dblclick', // shorthand }, }; <DataGrid rows={rows} gridConfig={gridConfig} /> ``` :::note[Alternative: feature props] You can instead pass `columns={columns}`, `selection="row"`, `filtering={{ debounceMs: 300 }}`, and `editing="dblclick"` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```vue <script setup> import '@toolbox-web/grid-vue/features/selection'; import '@toolbox-web/grid-vue/features/filtering'; import '@toolbox-web/grid-vue/features/editing'; import type { GridConfig } from '@toolbox-web/grid-vue'; const gridConfig: GridConfig = { columns, features: { selection: 'row', filtering: { debounceMs: 300 }, editing: 'dblclick', }, }; </script> <TbwGrid :rows="rows" :grid-config="gridConfig" /> ``` :::note[Alternative: feature props] You can instead bind `:columns="columns"`, `selection="row"`, `:filtering="{ debounceMs: 300 }"`, and `editing="dblclick"` directly on the grid. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/selection'; import '@toolbox-web/grid-angular/features/filtering'; import '@toolbox-web/grid-angular/features/editing'; import type { GridConfig } from '@toolbox-web/grid-angular'; gridConfig: GridConfig = { columns, features: { selection: 'row', filtering: { debounceMs: 300 }, editing: 'dblclick', }, }; // In template: // <tbw-grid [rows]="rows" [gridConfig]="gridConfig" /> ``` :::note[Alternative: per-feature directives] You can instead import `GridSelectionDirective`, `GridFilteringDirective`, and `GridEditingDirective` and bind `[selection]`, `[filtering]`, and `[editing]` directly on `<tbw-grid>`. That form gives a compile-time check that the features are imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: ## 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.<name>` 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` | | `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) => '<div>…</div>' }` | | `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 `<tbw-grid>`) | 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`](https://toolboxjs.com/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](https://toolboxjs.com/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 | --- # 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. **See also:** [Accessibility Conformance Report](https://toolboxjs.com/grid/guides/conformance-report.md) — the VPAT-style document for procurement, covering every WCAG 2.2 criterion and the EN 301 549 mapping · [Touch Input](https://toolboxjs.com/grid/guides/touch-input.md) — gesture map, long-press priority order, and touch browser quirks. ## Our accessibility commitment We target **WCAG 2.2 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. - **Focus is never hidden** — plugins that overlay the rows viewport report the band they obscure, so keyboard navigation scrolls the focused cell clear of them rather than underneath. [Sticky rows](https://toolboxjs.com/grid/plugins/sticky-rows.md) report the top/bottom band they cover (vertical scrolling); [pinned columns](https://toolboxjs.com/grid/plugins/pinned-columns.md) report the left/right band (horizontal scrolling). - **Every shipped theme meets AA, and two meet AAA** — all six themes clear 4.5:1 on every text pair in both light and dark mode; `dg-theme-contrast.css` and `dg-theme-large.css` go further to **Level AAA** (7:1). See [Theme contrast](#theme-contrast). See the [full WCAG 2.2 AA conformance table](#wcag-compliance-checklist) below for the criteria we test against, or the [Accessibility Conformance Report](https://toolboxjs.com/grid/guides/conformance-report.md) for the criterion-by-criterion VPAT-style breakdown, the evidence behind each claim, and the boundary between what the grid guarantees and what your application must supply. > **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 | |---------|------|-----------| | `<tbw-grid>` | `grid` (or `treegrid` when [Tree](https://toolboxjs.com/grid/plugins/tree.md) or [Row Grouping](https://toolboxjs.com/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](https://toolboxjs.com/grid/plugins/tree.md) or [Row Grouping](https://toolboxjs.com/grid/plugins/grouping-rows.md) plugin is active (1-based hierarchical depth) | `1`, `2`, … | | `aria-setsize` | [Tree](https://toolboxjs.com/grid/plugins/tree.md) or [Row Grouping](https://toolboxjs.com/grid/plugins/grouping-rows.md) plugin is active (sibling count at this level) | integer | | `aria-posinset` | [Tree](https://toolboxjs.com/grid/plugins/tree.md) or [Row Grouping](https://toolboxjs.com/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 | |-----|--------| | <kbd>↑</kbd> / <kbd>↓</kbd> | Move focus between rows | | <kbd>←</kbd> / <kbd>→</kbd> | Move focus between cells | | <kbd>Home</kbd> | Move to first cell in row | | <kbd>End</kbd> | Move to last cell in row | | <kbd>Ctrl</kbd> + <kbd>Home</kbd> | Move to first cell in grid | | <kbd>Ctrl</kbd> + <kbd>End</kbd> | Move to last cell in grid | | <kbd>PgUp</kbd> | Scroll up one viewport | | <kbd>PgDn</kbd> | Scroll down one viewport | | <kbd>⇥ Tab</kbd> | Move to next cell (wraps to next row) | | <kbd>⇧ Shift</kbd> + <kbd>⇥ Tab</kbd> | 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**](https://toolboxjs.com/grid/plugins/selection.md#keyboard-shortcuts) — <kbd>Space</kbd>, <kbd>Shift</kbd> + arrows / <kbd>PgUp</kbd> / <kbd>PgDn</kbd> / <kbd>Ctrl</kbd> + <kbd>Home</kbd>/<kbd>End</kbd>, <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Esc</kbd> - [**Editing**](https://toolboxjs.com/grid/plugins/editing.md#keyboard-shortcuts-row-mode) — <kbd>Enter</kbd> (start row edit / commit), <kbd>F2</kbd> (single-cell edit), <kbd>Tab</kbd> / <kbd>Shift</kbd> + <kbd>Tab</kbd>, <kbd>Esc</kbd> (cancel) - [**Clipboard**](https://toolboxjs.com/grid/plugins/clipboard.md#keyboard-shortcuts) — <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>C</kbd> / <kbd>X</kbd> / <kbd>V</kbd> - [**Context Menu**](https://toolboxjs.com/grid/plugins/context-menu.md) — <kbd>Shift</kbd> + <kbd>F10</kbd> or the dedicated <kbd>☰ Menu</kbd> key opens the menu at the focused cell; <kbd>↑</kbd>/<kbd>↓</kbd> navigates, <kbd>Enter</kbd>/<kbd>Space</kbd> activates, <kbd>Esc</kbd> closes - [**Row Grouping**](https://toolboxjs.com/grid/plugins/grouping-rows.md) — <kbd>Space</kbd> toggles expand/collapse on a group or tree node ## Focus Management ### Focus Indicators The grid uses visible focus indicators that meet WCAG 2.2 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 <tbw-grid aria-label="Employee directory"> ``` Or reference a visible heading: ```html <h2 id="grid-heading">Employees</h2> <tbw-grid aria-labelledby="grid-heading"> ``` ### 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") - Empty state ("No data to display") The empty-state overlay is itself `role="presentation"`. It renders inside the grid's row container, and `role="grid"` may only own rows and rowgroups — so the message is voiced through the grid's own live region instead of being marked up as a status of its own. ### Configuring Announcements Live region announcements are controlled by the [`a11y`](https://toolboxjs.com/grid/api/core/interfaces/a11yconfig.md) config. Turn them off entirely if your app already announces these state changes itself: ```typescript grid.gridConfig = { a11y: { announcements: false }, }; ``` Individual messages are replaced through `a11y.messages` — see [`A11yMessages`](https://toolboxjs.com/grid/api/core/interfaces/a11ymessages.md) for the full list and their signatures. Since the defaults are English, that override is also the translation hook: - [Translating announcements](https://toolboxjs.com/grid/guides/i18n.md#screen-reader-announcements): Overriding a11y.messages, plus the locale map for plugin UI strings ### 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 **What is automated, and what is not.** Every pull request runs three kinds of automated accessibility check: [axe-core](https://github.com/dequelabs/axe-core) scans of the grid and of the documentation site, computed-style measurement of focus indicators, contrast and target size, and [ARIA tree snapshots](https://playwright.dev/docs/aria-snapshots) that pin the accessibility tree of the grid skeleton, the disclosure controls, and transient surfaces such as the filter panel and context menu. Those catch rule violations and silent regressions in the exposed semantics. They do **not** tell you what a screen reader actually says. No assistive technology is driven in CI — announcements depend on the AT, the browser, and the user's verbosity settings, none of which a headless browser models. Screen-reader verification is therefore a **manual** procedure, run against a release candidate. The checklist below is that procedure; it is also the one to follow when validating your own app. #### Manual verification checklist Run each row in at least one Windows pairing (NVDA + Chrome or Firefox) and one macOS pairing (VoiceOver + Safari). A flow passes when the announcement carries the information in the last column — the exact wording varies by AT and is not itself a defect. | # | Flow | Steps | Must be announced | | - | ---- | ----- | ----------------- | | 1 | Enter the grid | Tab to the grid | Accessible name, `grid` (or `treegrid`) role, row and column counts | | 2 | Move between cells | <kbd>↓</kbd> / <kbd>→</kbd> | Cell value, its column header, and the row/column position | | 3 | Sort a column | Focus a header, press <kbd>Enter</kbd> | New sort direction — and the header name **without** the sort glyph or the resize handle's label | | 4 | Resize a column | Tab to the resize handle, <kbd>←</kbd>/<kbd>→</kbd> | The handle's own name (`Width of column <name>`) and the resulting width | | 5 | Select a row | <kbd>Space</kbd>, or activate the row checkbox | Selected/unselected state, and the new selection count if a status region is enabled | | 6 | Select all | Activate the header checkbox | `Select all rows`, and the resulting checked/mixed state | | 7 | Open the filter panel | Activate a column's filter button | That a panel opened, its name, and the first control inside it receiving focus | | 8 | Apply a filter | Change a value, activate **Apply** | The filtered row count, via the live region | | 9 | Expand a tree node | <kbd>→</kbd> on a parent row | Expanded state and the new child count; the row's name must not be swallowed by the glyph | | 10 | Expand a detail row | Activate the row's toggle | The button's name flipping between `Expand details` and `Collapse details`, plus its expanded state | | 11 | Edit a cell | <kbd>Enter</kbd> to open the editor | The editor's role and current value, and that focus moved into it | | 12 | Commit an edit | <kbd>Enter</kbd> | Confirmation of the committed value; on <kbd>Escape</kbd>, that the edit was discarded | | 13 | Open the context menu | <kbd>Shift</kbd>+<kbd>F10</kbd> or the menu key | `menu` role, item count, and each item as you arrow through | | 14 | Escape every transient surface | <kbd>Escape</kbd> from 7, 11, 13 | That the surface closed **and** where focus landed | Record the AT + browser + version for each run. A flow that regresses between releases is a bug worth filing even when axe still scores the page clean — that is precisely the class of defect automation cannot see. #### Expected announcements, by AT **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 label>, grid, <N> rows, <M> columns"` followed by the focused cell. 3. Press <kbd>↓</kbd> — `"<value>, column <name>, row <n> of <total>"`. 4. Press <kbd>Enter</kbd> on a header — `"sorted ascending"` / `"sorted descending"` (and the announcement is repeated via the live region). 5. Use NVDA's *Browse Mode* toggle (<kbd>Insert</kbd>+<kbd>Space</kbd>) to switch to virtual cursor — table-reading shortcuts (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+arrows) should walk the grid as a native HTML table. **VoiceOver (macOS, Safari):** 1. Enable VoiceOver: <kbd>⌘</kbd>+<kbd>F5</kbd>. 2. Use <kbd>VO</kbd>+<kbd>→</kbd> to enter the grid — you should hear the label and dimensions. 3. <kbd>VO</kbd>+<kbd>Shift</kbd>+<kbd>↓</kbd> enters interaction mode; then arrow keys navigate cells with full column + row context. 4. Rotor (<kbd>VO</kbd>+<kbd>U</kbd>) → *Tables* should list the grid for jump-navigation. **JAWS (Windows):** - Use *Virtual PC Cursor* (default) for browse, *Forms Mode* (<kbd>Enter</kbd>) for grid interaction. Table layer (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+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 ## Pointer Alternatives to Dragging WCAG 2.2 [SC 2.5.7 Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) (Level AA) requires every operation that uses a dragging movement to *also* be achievable with a single pointer that never drags. It targets people who cannot reliably press, hold, move and release in one uninterrupted gesture — tremor, limited dexterity, head pointers, eye-gaze, switch devices and speech-driven mouse emulation. A keyboard shortcut does not satisfy it. The W3C is explicit that keyboard equivalence only counts when the same operation is *also* exposed through controls a pointer can click or tap. The grid therefore ships a pointer path for every drag: | Drag operation | Pointer alternative | | ------------------- | ------------------------------------------------------------------------------ | | Resize a column | **Tap** (don't drag) the resize handle → a popover with − / width / + / Reset | | [Reorder a column](https://toolboxjs.com/grid/plugins/reorder-columns.md#accessibility) | Right-click, long-press or <kbd>Shift</kbd> + <kbd>F10</kbd> the header → Move left / right / to start / to end | | [Reorder a column group](https://toolboxjs.com/grid/plugins/reorder-columns.md#accessibility) | Same menu on the group header cell → moves the whole fragment past its whole neighbour | | [Reorder a row](https://toolboxjs.com/grid/plugins/row-drag-drop.md#accessibility) | **Tap** the row drag handle → Move up / down / to top / to bottom | | [Move a row to another grid](https://toolboxjs.com/grid/plugins/row-drag-drop.md#accessibility) | Same menu → Copy to / Send to, one entry per connected grid | | [Select a cell range](https://toolboxjs.com/grid/plugins/selection.md#accessibility) | Click the first cell, then right-click / long-press the opposite corner → Extend selection to here | | [Resize a cell range](https://toolboxjs.com/grid/plugins/selection.md#accessibility) | **Tap** a range corner handle to arm it, then tap the cell that corner should move to | | [Resize the tool panel](https://toolboxjs.com/grid/plugins/shell.md#accessibility) | **Tap** the tool-panel splitter → the same − / width / + / Reset popover | | [Move a pivot field](https://toolboxjs.com/grid/plugins/pivot.md#accessibility) | **Click** the field chip → Move up / down / to the other group zone / to Values / Remove | | [Reorder from the column panel](https://toolboxjs.com/grid/plugins/visibility.md#accessibility) | **Click** the panel's drag handle → Move up / down / to top / to bottom (group handles move the whole block) | ### Keeping it out of the way By design these alternatives reuse an affordance that already exists, so they cost no extra header width or visual noise. Three rules keep them from interfering with ordinary pointer use: 1. **No persistent chrome on hovering pointers.** Nothing is added to the default UI. The inline buttons below are opt-in, and appear unprompted only where hover does not exist. 2. **No gesture is taken over that the grid or the browser already answers.** The one exception is the range-extend entry: with no `ContextMenuPlugin` registered, the SelectionPlugin hosts a bare menu on right-click over a cell, which suppresses the native menu. Register the [ContextMenuPlugin](https://toolboxjs.com/grid/plugins/context-menu.md) and the entry is injected into your menu instead, alongside your own items. 3. **Nothing fires on the first half of an existing double-click.** Tapping the resize handle waits out the double-click window (300 ms) before offering the width popover, so double-clicking the handle still resets the column without the popover flashing up. If you would rather trade density for discoverability, switch the alternatives to dedicated buttons that are revealed on hover or focus: ```ts gridConfig = { a11y: { // 'menu' (default) — reached through the existing handle or context menu // 'inline' — also render a dedicated button per affected header dragAlternatives: 'inline', }, }; ``` Pointers that cannot hover (touch, most switch devices) always get the inline controls regardless of this setting — there is nothing for them to hover in order to reveal a control. ## Target size WCAG 2.2 SC 2.5.8 asks that every pointer target measure at least 24 × 24 CSS pixels. It says nothing about the pointer type, so the minimum has to hold for a mouse as much as for a finger — and a grid whose whole value is density cannot answer that by making every row taller. So the grid grows the **target**, not the **box**. Row heights, column widths, tree indentation and icon sizes are exactly what they were before the minimum existed: | Control | How it reaches 24 px | |---|---| | Tree, row-group, pivot and master-detail toggles; the row drag handle; the filter button | A transparent overlay centred on the icon | | Column resize handle | Absolutely positioned over the column border, so its width costs no layout. On the **last** column there is no neighbouring column to reach into, so the handle sits fully inside the cell instead and the cell's trailing padding grows to match | | Select-all checkbox | Wrapped in a `<label>`, which makes the whole header cell the target | | Row checkbox | The row click already selects, so the target is the cell | | Tool-panel splitter | Straddles the panel's docked edge, half outside, so it never covers the panel's own controls | One small target is never allowed to overlap another. Where a resize handle would reach into the filter button beside it, the header cell's trailing padding grows instead — by about four pixels, once, on resizable columns. A coarse pointer additionally enlarges the *visible* box of several controls. That is a comfort choice layered on top of the target minimum, not the thing that satisfies it — the 24 px holds on a mouse either way. Everything is driven from one token, so raising the minimum raises every target at once: ```css tbw-grid { --tbw-touch-target-min: 44px; /* SC 2.5.5 Target Size (Enhanced), AAA */ --tbw-tool-panel-resize-overhang: 22px; /* half of it, for the splitter */ } ``` 44 px also matches Apple's Human Interface Guidelines recommendation for primary touch targets. The [Shell](https://toolboxjs.com/grid/plugins/shell.md) tool-panel splitter is the one control that needs a second token. Because its target straddles the panel's docked edge, half of it sits outside the panel — which means the panel's clip region has to widen to match. Browsers reject a calculated `overflow-clip-margin`, so the overhang cannot be derived from `--tbw-touch-target-min` and has to be stated separately: | Token | Default | Notes | |---|---|---| | `--tbw-touch-target-min` | `24px` | Every pointer target in the grid | | `--tbw-tool-panel-resize-overhang` | `12px` | Half the splitter's target width. Raise it alongside the token above. | ## Reflow and text spacing Two criteria govern what happens when the page gets narrow or the reader makes the text roomier. Both come down to the same promise: **nothing may become unreachable.** ### Reflow (SC 1.4.10) [SC 1.4.10 Reflow](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html) asks that content survive a 320 CSS-pixel viewport without a second scrollbar — but it exempts content that "requires two-dimensional layout for usage or meaning", and it names data tables as the example. A grid is that exemption: the table keeps its horizontal scrollbar, because collapsing columns into a single file would destroy the row-to-column relationships that make it a table at all. The exemption covers the table and nothing else. Everything the grid puts *around* the table has to reflow properly: - **The tool panel is clamped to the grid it docks into.** A panel wider than its container would be clipped with no scrollbar to reach what was cut off — the chip labels and half the section headings would simply be gone. Both the auto-open width and a width you dragged yourself are held to the container. - **The header collapses gracefully.** The title truncates before the toolbar buttons do, so the controls stay reachable no matter how narrow the grid gets. - **Card mode is available for the table itself.** If you would rather the table reflowed than scrolled, the [Responsive plugin](https://toolboxjs.com/grid/plugins/responsive.md) restacks each row as a card below a breakpoint you choose. It watches the grid element, not the viewport, so a grid in a narrow sidebar reflows on a wide screen too. ### Text spacing (SC 1.4.12) [SC 1.4.12 Text Spacing](https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html) lets a reader override line height, letter spacing and word spacing, and requires that no content or function be lost as a result. Text set that way needs roughly a third more room, which is exactly the room a grid does not have. Vertically the grid gives way. Rows are sized from a measurement of the rendered content rather than a fixed height, so a taller line box makes the row taller and the virtualized layout follows it — text never overflows its row or collides with the row below. Horizontally a column has a fixed width, so wider text runs past it and picks up an ellipsis. The SC permits that, on one condition: the hidden text must still be available. So **hovering a cell whose text does not fit reveals the whole value**. The title is resolved on hover rather than at render time, so cells that fit stay tooltip-free and the render path is untouched. Install the [Tooltip plugin](https://toolboxjs.com/grid/plugins/tooltip.md) and it takes over with a styled popover that is keyboard-reachable, hoverable and dismissible with <kbd>Escape</kbd>. If you would rather nothing truncated at all, let cells wrap: ```css tbw-grid { --tbw-cell-white-space: normal; } ``` Rows then grow to fit their tallest cell. This costs vertical space on every row, so it is a choice rather than the default — but it composes with everything else, including virtualization. ## Reduced Motion The grid respects the `prefers-reduced-motion: reduce` user preference automatically. When set, the grid: - **Disables row animations** ([row animation API](https://toolboxjs.com/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'; ``` ### Theme contrast **Every shipped theme clears WCAG Level AA**, and two clear Level AAA. There is no theme in the box that you have to trade accessibility for. | Theme | Import | Text bar | Aimed at | |-------|--------|----------|----------| | Contrast | `dg-theme-contrast.css` | **AAA** (7:1) | Low vision, glare, and users who need maximum separation | | Large | `dg-theme-large.css` | **AAA** (7:1) | Low vision and motor impairment — 18px base text and larger hit targets | | Standard | `dg-theme-standard.css` | AA (4.5:1) | A neutral, modern default | | Bootstrap | `dg-theme-bootstrap.css` | AA (4.5:1) | Matching a Bootstrap 5 application | | Material | `dg-theme-material.css` | AA (4.5:1) | Matching a Material 3 application | | Vibrant | `dg-theme-vibrant.css` | AA (4.5:1) | A saturated, high-energy identity | The bar applies to *every* foreground/background pair the theme produces — body text, muted text, header text, alternating rows, hovered rows, selected rows, and text on the accent colour — in both light and dark mode. Translucent row tints are composited over the grid background before they are scored, so the ratio reflects what is actually painted. Non-text UI colours (the focus ring, and the strong border that bounds the cell editor's input) clear SC 1.4.11's 3:1. All of it is asserted in a real browser on every CI run by `apps/docs-e2e/tests/theme-contrast.spec.ts`, so a token edit that drops any pair below its theme's bar fails the build. :::caution[Overriding tokens can break the guarantee] The guarantee covers the tokens as shipped. If you override `--tbw-color-fg`, `--tbw-color-accent`, `--tbw-color-header-fg`, or any background token, re-check the pairs yourself — the [Theme Builder's contrast panel](https://toolboxjs.com/grid/guides/theming.md#interactive-theme-builder) reports every pair live. ::: :::tip[Accent labels flip in dark mode] Several themes use a light accent in dark mode, so the text drawn *on* the accent is dark rather than white — the same thing Bootstrap and Material do in their own dark palettes. If you override `--tbw-color-accent`, override `--tbw-color-accent-fg` to match, using `light-dark()` so each mode gets the readable label. ::: ### 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 }) => `<span class="dot dot-${value}"></span>` } // ✅ Text + color — readable everywhere { field: 'status', renderer: ({ value }) => `<span class="dot dot-${value}" aria-label="${value}">${value}</span>` } ``` ### 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 `<button>` / `<a>` inside renderers Custom action buttons should be real `<button>` elements (or `<a href>` for navigation), not clickable `<div>`s. They inherit keyboard activation, focus, and screen-reader role for free. The grid's roving-tabindex pattern still works because the renderer is inside a `gridcell` — pressing <kbd>Enter</kbd> activates the button. ### 4. Label fragmented controls If a cell renders multiple controls (e.g. edit + delete buttons), each needs a discrete accessible name — usually via `aria-label`: ```ts viewRenderer: ({ row }) => ` <button aria-label="Edit ${row.name}">✏️</button> <button aria-label="Delete ${row.name}">🗑️</button> ` ``` 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. ## 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](https://toolboxjs.com/grid/plugins/tree.md) or [Row Grouping](https://toolboxjs.com/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 (`<tbw-grid-header 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 `<tbw-grid-header title="...">`, 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 --- # 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](https://toolboxjs.com/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="<col>"`, `data-row="<rowIndex>"`, `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](https://toolboxjs.com/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<T>` — 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<void>((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<void>((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<void> { 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`](https://toolboxjs.com/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. --- # Internationalization > Translating @toolbox-web/grid — screen reader announcements, plugin UI strings, cell value formatting, and right-to-left layout. The grid ships **English defaults** and gives you an override point for every string it produces. There is no locale bundle to load and no i18n runtime — you supply strings from whatever i18n system your app already uses. There are two override surfaces, split by what the strings are: | Config | Covers | Shape | | ------ | ------ | ----- | | [`a11y.messages`](#screen-reader-announcements) | Live-region announcements read by screen readers | Functions of runtime values | | [`locale`](#built-in-ui-strings-locale) | Visible plugin UI — buttons, labels, placeholders | Flat key → string map | Everything else (column headers, menu labels, cell values) is text you already supply, so you translate it before it reaches the grid — see [Other localizable surfaces](#other-localizable-surfaces). ## Screen reader announcements Live-region announcements — sort, filter, selection, grouping, editing — are the grid's largest body of user-facing text. Override any subset via `a11y.messages`; anything you omit falls back to the English default. ```ts grid.gridConfig = { a11y: { messages: { sortApplied: (column, direction) => `Trié par ${column}, ${direction}`, sortCleared: () => 'Tri effacé', filterApplied: (column) => `Filtre appliqué sur ${column}`, filterCleared: (column) => `Filtre effacé de ${column}`, allFiltersCleared: () => 'Tous les filtres effacés', groupExpanded: (name, count) => `Groupe ${name} développé, ${count} lignes`, groupCollapsed: (name) => `Groupe ${name} réduit`, selectionChanged: (count) => `${count} ligne(s) sélectionnée(s)`, editingStarted: (rowIndex) => `Édition de la ligne ${rowIndex + 1}`, editingCommitted: (rowIndex) => `Ligne ${rowIndex + 1} enregistrée`, dataLoaded: (count) => `${count} lignes chargées`, }, }, }; ``` Every message is a function, so you can interpolate, pluralize, or delegate to `Intl.PluralRules` however your locale requires. :::note[Not every message is announced] Two keys — `selectAllRows` and `selectRow` — are **accessible names** rather than live-region announcements. The [Selection plugin](https://toolboxjs.com/grid/plugins/selection.md) uses them to label its checkbox column, which would otherwise be read as a bare "checkbox": ```ts a11y: { messages: { selectAllRows: () => 'Sélectionner toutes les lignes', selectRow: (rowIndex) => `Sélectionner la ligne ${rowIndex + 1}`, }, } ``` `rowIndex` is zero-based, so add 1 for a human-facing position. ::: - [A11yMessages](https://toolboxjs.com/grid/api/core/interfaces/a11ymessages.md): The full list of overridable announcements and their signatures - [What triggers each one](https://toolboxjs.com/grid/guides/accessibility.md#live-regions): Live regions and the rest of the accessibility model ## Built-in UI strings (`locale`) The buttons, labels and placeholders that plugins render into their own UI — the filter panel, the columns tool panel, the pivot builder — are localized through a single flat key/value map on `gridConfig.locale`. ```ts grid.gridConfig = { locale: { 'filter.search': 'Rechercher...', 'filter.apply': 'Appliquer', 'filter.clear': 'Effacer le filtre', 'columns.panelTitle': 'Colonnes', 'pivot.grandTotal': 'Total général', }, }; ``` :::note[No locale bundle to load] The grid ships **no default locale map**. Each call site passes its English string inline as the fallback, so an omitted key stays English and a plugin you do not load contributes nothing to your bundle. `locale` is read live, so swapping the map at runtime re-localizes on the next render. ::: `locale` is separate from `a11y.messages` on purpose: these are static labels, while announcements are functions of runtime values (row counts, column names, sort direction). ### Key reference Keys are namespaced by the plugin that owns them. Only load-bearing UI text is keyed; anything you already supply (column headers, menu item labels, export filenames) stays under your control. | Key | English default | Plugin | | --- | --------------- | ------ | | `filter.search` | `Search...` | Filtering | | `filter.selectAll` | `Select All` | Filtering | | `filter.noMatches` | `No matching values` | Filtering | | `filter.apply` | `Apply` | Filtering | | `filter.clear` | `Clear Filter` | Filtering | | `filter.clearAll` | `Clear All Filters` | Filtering | | `filter.min` | `Min` | Filtering (number) | | `filter.max` | `Max` | Filtering (number) | | `filter.from` | `From` | Filtering (date) | | `columns.panelTitle` | `Columns` | Column Visibility | | `columns.hideColumn` | `Hide column` | Column Visibility | | `columns.showAll` | `Show all` | Column Visibility | | `columns.dragHandle` | `Drag to reorder` | Column Visibility | | `columns.dragGroupHandle` | `Drag to reorder group` | Column Visibility | | `pinnedColumns.pinLeft` | `Pin Left` | Pinned Columns | | `pinnedColumns.pinRight` | `Pin Right` | Pinned Columns | | `pinnedColumns.unpin` | `Unpin` | Pinned Columns | | `print.buttonTitle` | `Print grid` | Print | | `pivot.panelTitle` | `Pivot` | Pivot | | `pivot.options` | `Options` | Pivot | | `pivot.enable` | `Enable pivot` | Pivot | | `pivot.rowGroups` | `Row Groups` | Pivot | | `pivot.columnGroups` | `Column Groups` | Pivot | | `pivot.values` | `Values` | Pivot | | `pivot.availableFields` | `Available Fields` | Pivot | | `pivot.filterFields` | `Filter fields...` | Pivot | | `pivot.allFieldsUsed` | `All fields in use` | Pivot | | `pivot.dropFields` | `Drop fields here` | Pivot | | `pivot.dropNumericFields` | `Drop numeric fields here` | Pivot | | `pivot.removeField` | `Remove field` | Pivot | | `pivot.removeValueField` | `Remove value field` | Pivot | | `pivot.aggFunction` | `Aggregation function` | Pivot | | `pivot.customAgg` | `Custom` | Pivot | | `pivot.showRowTotals` | `Show row totals` | Pivot | | `pivot.showGrandTotal` | `Show grand total` | Pivot | | `pivot.grandTotal` | `Grand Total` | Pivot | ### In custom renderers and plugins A custom `filterPanelRenderer` receives the same lookup on its params, so your own panel localizes through the app's single map: ```ts filterPanelRenderer: (container, params) => { const apply = document.createElement('button'); apply.textContent = params.t('filter.apply', 'Apply'); container.appendChild(apply); }, ``` Inside a custom plugin, `BaseGridPlugin` exposes the same helper as `this.t(key, fallback)` (and `this.translate` when you need to hand the function to a render module). ## Other localizable surfaces | Surface | How to localize | | ------- | --------------- | | Column headers | `columns[].header` — pass an already-translated string | | Empty / loading state | A custom `emptyRenderer` / `loadingRenderer` returning translated markup | | Cell values (dates, numbers, currency) | `columns[].format` with `Intl.NumberFormat` / `Intl.DateTimeFormat` | | Filter panel labels | `locale` keys (`filter.*`), or a custom `filterPanelRenderer` | | Context menu items | Context Menu plugin `items[].label` | | Tool panel titles | `title` on `<tbw-grid-tool-panel>` / the shell `toolPanel` config | | Export filenames and sheet names | Export plugin config | ## Formatting values Use `Intl` in a column `format` function rather than pre-formatting your data — this keeps sorting and filtering operating on the underlying value: ```ts { field: 'salary', type: 'number', format: (value) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', }).format(value as number), } ``` ## Right-to-left Rows and headers are laid out with CSS Grid (a single `grid-template-columns` track list shared by every row), so column order follows the inline axis and mirrors automatically when the direction flips. Together with logical CSS properties, that means the grid inherits direction from its ancestors — set `dir="rtl"` on a container (or `<html>`) and it mirrors: ```html <div dir="rtl"> <tbw-grid id="grid"></tbw-grid> </div> ``` Behaviour that CSS alone cannot mirror resolves the direction at runtime: arrow-key navigation swaps left/right, and [pinned columns](https://toolboxjs.com/grid/plugins/pinned-columns.md) accept the logical `'start'` / `'end'` values so the same config pins correctly in both directions. Custom renderers and custom themes are your responsibility — prefer logical properties (`margin-inline-start`, `padding-inline`, `inset-inline-end`) over physical ones so your additions mirror too. --- # Performance > Optimize @toolbox-web/grid for large datasets — virtualization tuning, bundle optimization, rendering best practices, and benchmarks. `@toolbox-web/grid` is engineered for high performance out of the box. This guide helps you understand the performance characteristics and tune the grid for your specific workload. :::tip[Looking for benchmarks?] We publish a head-to-head benchmark against AG Grid, Tabulator, and SlickGrid that **runs live in your browser** — render 1k / 10k / 100k rows side-by-side and watch the numbers. See [Compared to other grids](https://toolboxjs.com/grid/comparison.md) for the methodology, results, and a feature matrix. ::: ## Why it's fast A short explanation lives here so you don't have to dig — for the full architectural narrative (render scheduler, virtualization, DOM recycling, template cloning, faux scrollbar) see the [Architecture deep-dive](https://toolboxjs.com/grid/architecture.md). | Technique | What it buys you | |---|---| | **Centralized render scheduler** | All updates coalesce into a single `requestAnimationFrame` — no layout thrashing, no double work | | **Row + column virtualization** | Rendering 100k rows costs the same as rendering 50 | | **DOM recycling with epoch invalidation** | Row elements are reused via a pool — minimal GC, no element churn | | **Template cloning** (`cloneNode(true)`) | 3-4× faster than `createElement` + attribute assignment | | **Event delegation** | One listener per event type on the container — scales to any dataset | | **Faux scrollbar** | Scroll container is separate from the rendered content — zero reflow on scroll | | **Phase-priority execution** | Highest-priority phase wins per frame — full re-renders absorb sub-renders | ## Key Performance Features ### Row Virtualization The grid only renders rows visible in the viewport plus a configurable overscan buffer. This means rendering 100,000 rows is as fast as rendering 50 rows. ```typescript grid.gridConfig = { columns: [...], rowHeight: 28, // Fixed row height in pixels (default: auto-measured) }; ``` - **Default row height:** Auto-measured from first row (respects `--tbw-row-height` CSS variable) - **Variable row heights:** Supported via `rowHeight: (row, index) => number | undefined` ### Column Virtualization For grids with many columns (50+), enable column virtualization: ```typescript import '@toolbox-web/grid/features/column-virtualization'; grid.gridConfig = { columns: manyColumns, features: { columnVirtualization: true }, }; ``` ### Render Scheduler All rendering is batched through a centralized `RenderScheduler` that coalesces multiple update requests into a single `requestAnimationFrame` callback. **Render phases (lowest → highest priority):** | Phase | Priority | What It Does | |-------|----------|-------------| | STYLE | 1 | CSS custom property updates | | VIRTUALIZATION | 2 | Scroll position + visible window recalc | | HEADER | 3 | Header row re-render | | ROWS | 4 | Data row re-render | | COLUMNS | 5 | Column structure rebuild | | FULL | 6 | Complete re-render | When multiple phases are requested in the same frame, only the highest-priority phase executes (it inherently covers lower phases). ## Bundle Size Optimization ### Tree-Shaking Features The most important optimization: **only import the features you use**. ```typescript // ✅ Optimal: ~45 KB core + only features you need import '@toolbox-web/grid'; import '@toolbox-web/grid/features/selection'; import '@toolbox-web/grid/features/editing'; // ❌ Heavy: imports ALL plugins even if you use two import '@toolbox-web/grid/all'; ``` ### Budget Reference | Bundle | Raw | Gzipped | |--------|-----|---------| | Core (`index.js`) | ≤ 170 KB | ≤ 50 KB (soft warning at 45 KB) | | Individual plugin | 2–15 KB | 1–5 KB | | All plugins (`all.js`) | ~300 KB | ~80 KB | ## Tuning for Large Datasets ### Fixed Row Heights Variable row heights (`rowHeight: (row) => number`) require measuring each row, which is slower than fixed heights. For datasets over 10,000 rows, prefer fixed heights: ```typescript grid.gridConfig = { rowHeight: 32, }; ``` ### Disable Animations Row animations add visual polish but cost CPU cycles. Disable them for very large datasets: ```typescript grid.gridConfig = { animation: { mode: 'off' }, }; ``` By default `mode` is `'reduced-motion'` — animations are already disabled when the user has `prefers-reduced-motion: reduce` set. ### Batch Data Updates When updating many rows, assign the entire array at once rather than using `insertRow()` / `removeRow()` in a loop: ```typescript // ✅ Single assignment — triggers one sort/filter/render cycle grid.rows = updatedData; // ❌ Loop — triggers N animations and N re-renders for (const newRow of newRows) { await grid.insertRow(0, newRow); } ``` ### Debounce Filtering When using filtering with a large dataset, increase the debounce to reduce re-filtering: ```typescript import '@toolbox-web/grid/features/filtering'; grid.gridConfig = { features: { filtering: { debounceMs: 300 } }, // Default: 200 }; ``` ### Custom Sort Comparators For fields with expensive comparisons, provide a custom `sortComparator`: ```typescript { field: 'name', sortable: true, sortComparator: (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: 'base' }), } ``` ## Rendering Best Practices ### Avoid Direct DOM Manipulation Let the grid manage its DOM. Direct manipulation can conflict with the render scheduler and virtualization: ```typescript // ❌ Don't do this — bypasses the grid's render cycle document.querySelector('.data-grid-row')!.style.background = 'red'; // ✅ Use rowClass/cellClass or a renderer instead grid.gridConfig = { rowClass: (row) => row.status === 'error' ? 'row-error' : '', }; ``` ### Use `registerStyles()` for Dynamic Runtime CSS Standard CSS (stylesheets, `<style>` in `<head>`) works fine for static styles. For styles you need to inject or toggle **from JavaScript at runtime**, use `registerStyles()` instead of appending `<style>` elements inside the grid (which get removed by `replaceChildren()`): ```typescript grid.registerStyles('my-highlights', ` .highlight-row { background: yellow; } `); // Clean up when done grid.unregisterStyles('my-highlights'); ``` ### Prefer Formatters Over Renderers Formatters return a plain string — significantly faster than creating DOM elements via a renderer. Use a renderer only when you need interactive elements or custom HTML structure: ```typescript // ✅ Fast — formatter returns a string (no DOM creation) { field: 'salary', format: (v) => `$${v.toFixed(2)}` } // ⚠️ Slower — renderer creates DOM elements (use only when needed) { field: 'status', renderer: (ctx) => { const badge = document.createElement('span'); badge.className = `badge-${ctx.value}`; badge.textContent = String(ctx.value); return badge; }} ``` ## Profiling ### Scale-tier reference Use this table to set expectations and pick which knobs matter for your dataset. All numbers assume the default plugin set on a 2023-class laptop (M-series Mac / Ryzen 7). | Rows × Cols | What to expect | What you'll tune | |---|---|---| | **≤ 1k × 20** | Instant — no tuning needed | Nothing | | **10k × 20** | Smooth 60fps scroll / sort < 50ms | `rowHeight: number` (fixed) over variable; prefer `format` over `renderer` | | **100k × 20** | Smooth scroll; sort 200-500ms (single-threaded JS) | Same as above + `getRowId` for stable identity; consider [Server-Side](https://toolboxjs.com/grid/plugins/server-side.md) for queries | | **1M+ rows** | Don't load into memory — paginate or use [Server-Side](https://toolboxjs.com/grid/plugins/server-side.md) | Server-Side plugin with infinite scroll | | **× 50+ columns** | Horizontal scroll may stutter without column virtualization | Enable [`ColumnVirtualizationPlugin`](https://toolboxjs.com/grid/plugins/column-virtualization.md) | ### Profile your own app The "is the grid slow?" question is almost always one of: (a) **the grid is doing too much work** (too many columns rendered, expensive renderers, layout thrashing from outside the grid); or (b) **your data pipeline is slow** (re-allocating row arrays, re-deriving columns on every render, heavy formatters). The DevTools Performance tab can tell the two apart in 60 seconds. 1. **Record a representative interaction.** Open Chrome DevTools → Performance → click ⏺ Record → do the slow thing (scroll, sort, filter, edit) for 2-3 seconds → stop. 2. **Find the long tasks.** Anything > 50ms shows up as a red triangle. Click into the flame graph. 3. **Read the stack:** - **`renderScheduler.flush` dominates** → grid-internal work. Look one level deeper: - `renderRow` / `updateRowDom` heavy → your `renderer` or `cellClass` is slow. Switch to `format` if you only return text. - `measureRow` heavy → variable row heights with expensive content. Set a fixed `rowHeight` or memoize. - `applyColumns` heavy → too many visible columns. Enable [`ColumnVirtualizationPlugin`](https://toolboxjs.com/grid/plugins/column-virtualization.md). - **Framework code (React reconciler / Angular CD / Vue render) dominates** → your wrapper is re-rendering the grid on unrelated state changes. Pass `rows` / `gridConfig` as a stable reference (`useMemo`, `computed`, `signal`). - **`Recalculate Style` / `Layout` dominate, outside the grid** → something on the page is reacting to the grid's resize. Usually a parent flexbox or a `ResizeObserver` you wrote. 4. **Mark your own boundaries** to make the trace readable: ```ts performance.mark('app:load-rows:start'); grid.rows = bigArray; await new Promise(r => requestAnimationFrame(r)); performance.mark('app:load-rows:end'); performance.measure('app:load-rows', 'app:load-rows:start', 'app:load-rows:end'); ``` `performance.measure()` entries show up as labeled bars at the top of the flame chart. 5. **Compare against the [comparison page](https://toolboxjs.com/grid/comparison.md)** — if your numbers are very different from ours on a similar dataset, the cause is in your app code, not the grid. ### Key metrics to watch | Metric | Target | How to measure | |--------|--------|---------------| | **Scroll FPS** | 60fps | DevTools → Performance → Frames row | | **Sort time (10K rows)** | < 50ms | `console.time()` around `grid.rows = sorted` | | **Filter time (10K rows)** | < 30ms | Measure in `filter-change` event handler | | **Initial render** | < 100ms | Performance tab → First Paint | | **Memory (100K rows)** | < 100 MB | Memory tab → Heap snapshot | ## Stress Test Try the interactive stress test below — adjust row and column counts to see how the grid performs with large datasets: ```ts // PerformanceStressTestDemo.astro import '@toolbox-web/grid'; import type { ColumnConfig } from '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/column-virtualization'; import '@toolbox-web/grid/features/filtering'; import '@toolbox-web/grid/features/multi-sort'; import '@toolbox-web/grid/features/pinned-columns'; import '@toolbox-web/grid/features/selection'; // #region Types & Helpers type ExtendedColumnConfig = ColumnConfig & { sticky?: 'left' | 'right'; filterable?: boolean }; interface BenchmarkResult { name: string; category: 'render' | 'scroll' | 'operation' | 'memory'; time: number; unit: string; target: number; passed: boolean; note?: string; } function generateColumns(count: number, options?: { sortable?: boolean; filterable?: boolean }): ExtendedColumnConfig[] { const columns: ExtendedColumnConfig[] = [ { field: 'id', header: 'ID', type: 'number', width: 60, sortable: options?.sortable }, ]; for (let i = 1; i < count; i++) { columns.push({ field: `col${i}`, header: `Column ${i}`, type: 'string', width: 100, sortable: options?.sortable, filterable: options?.filterable, }); } return columns; } function generateRows(rowCount: number, columnCount: number): Record<string, unknown>[] { const rows: Record<string, unknown>[] = []; for (let i = 0; i < rowCount; i++) { const row: Record<string, unknown> = { id: i + 1 }; for (let j = 1; j < columnCount; j++) { row[`col${j}`] = `R${i + 1}C${j}`; } rows.push(row); } return rows; } function formatTime(ms: number): string { if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`; if (ms < 1000) return `${ms.toFixed(2)}ms`; return `${(ms / 1000).toFixed(2)}s`; } function formatBytes(bytes: number): string { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } // #endregion // #region Results Rendering function renderResultsTable(results: BenchmarkResult[], isComplete = false): string { const categories = ['render', 'scroll', 'operation', 'memory'] as const; const categoryLabels: Record<string, string> = { render: '🎨 Initial Render', scroll: '📜 Scroll Performance', operation: '⚡ Operations', memory: '💾 Memory', }; let html = `<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px;">`; for (const cat of categories) { const catResults = results.filter((r) => r.category === cat); if (catResults.length === 0) continue; html += ` <div style="background:var(--sl-color-gray-6,#1e1e1e);border-radius:6px;padding:12px;min-width:0;"> <h4 style="margin:0 0 10px 0;color:var(--sl-color-white,#e5e5e5);font-size:13px;font-weight:600;">${categoryLabels[cat]}</h4> <div style="display:grid;grid-template-columns:1fr auto auto auto;gap:4px 12px;font-size:12px;align-items:center;"> ${catResults.map((r) => ` <div style="color:var(--sl-color-white,#e5e5e5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${r.name}</div> <div style="color:${r.passed ? '#4ade80' : '#f87171'};font-family:monospace;text-align:right;"> ${r.unit === 'info' ? '—' : r.unit === 'bytes' ? formatBytes(r.time) : r.unit === 'bool' ? (r.time ? 'Yes' : 'No') : r.unit === 'count' ? r.time.toLocaleString() : formatTime(r.time)} </div> <div style="color:#888;font-family:monospace;text-align:right;font-size:11px;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${r.note || ''}"> ${r.note ? r.note : (r.unit === 'bool' || r.unit === 'info' || r.unit === 'count') ? '' : r.target === Infinity ? '(info)' : '<' + (r.unit === 'bytes' ? formatBytes(r.target) : formatTime(r.target))} </div> <div style="text-align:center;">${r.target === Infinity ? 'ℹ️' : r.passed ? '✅' : '❌'}</div> `).join('')} </div> </div> `; } html += `</div>`; if (isComplete) { const passed = results.filter((r) => r.passed).length; const total = results.length; const allPassed = passed === total; html += ` <div style="margin-top:16px;padding:12px;background:${allPassed ? '#166534' : '#991b1b'};border-radius:4px;text-align:center;"> <strong style="color:#fff;font-size:14px;">${allPassed ? '✅ All benchmarks passed!' : `⚠️ ${passed}/${total} benchmarks passed`}</strong> </div> `; } else { html += `<div style="margin-top:16px;padding:8px;opacity:0.6;text-align:center;font-size:12px;">Running... ${results.length} tests completed</div>`; } return html; } // #endregion // #region DOM References const grid = queryGrid('#demo-perf-stress')!; const rowsSlider = document.getElementById('stress-rows') as HTMLInputElement; const colsSlider = document.getElementById('stress-cols') as HTMLInputElement; const rowsVal = document.getElementById('stress-rows-val')!; const colsVal = document.getElementById('stress-cols-val')!; const runBtn = document.getElementById('stress-run') as HTMLButtonElement; const statusEl = document.getElementById('stress-status')!; const resultsEl = document.getElementById('stress-results')!; // #endregion // #region Slider Handlers function getRowCount() { return parseInt(rowsSlider.value, 10); } function getColCount() { return parseInt(colsSlider.value, 10); } rowsSlider.addEventListener('input', () => { rowsVal.textContent = getRowCount().toLocaleString(); updateStatus(); }); colsSlider.addEventListener('input', () => { colsVal.textContent = String(getColCount()); updateStatus(); }); function updateStatus() { statusEl.textContent = `Ready - ${getRowCount().toLocaleString()} rows × ${getColCount()} columns`; } function applyInitialData() { const rowCount = getRowCount(); const colCount = getColCount(); statusEl.textContent = `Ready - ${rowCount.toLocaleString()} rows × ${colCount} columns`; grid.gridConfig = { columns: generateColumns(colCount, { sortable: true, filterable: true }), fitMode: 'fixed', features: { selection: 'range', pinnedColumns: true, multiSort: { maxSortColumns: 3 }, filtering: { debounceMs: 0 }, ...(colCount >= 20 ? { columnVirtualization: { threshold: 20, overscan: 3 } } : {}), }, }; grid.rows = generateRows(rowCount, colCount); } // Show initial status without generating data updateStatus(); // #endregion // #region Benchmark Runner runBtn.addEventListener('click', async () => { runBtn.disabled = true; const allResults: BenchmarkResult[] = []; const rowCount = getRowCount(); const colCount = getColCount(); const updateResults = () => { resultsEl.innerHTML = renderResultsTable(allResults); }; const nextFrame = () => new Promise<void>((r) => requestAnimationFrame(() => r())); const measure = async (fn: () => void | Promise<void>): Promise<number> => { await nextFrame(); const start = performance.now(); await fn(); await nextFrame(); return performance.now() - start; }; // Scaling const baselineRows = 10_000; const baselineCols = 10; const rowScale = Math.max(1, Math.sqrt(rowCount / baselineRows)); const colScale = Math.max(1, Math.sqrt(colCount / baselineCols)); const cellScale = rowScale * colScale; const scaledByRows = (base: number) => Math.round(base * rowScale); const scaledByCells = (base: number) => Math.round(base * cellScale); const baseColumns = generateColumns(colCount, { sortable: true, filterable: true }); const baseRows = generateRows(rowCount, colCount); // 🎨 INITIAL RENDER statusEl.textContent = 'Testing: Initial render (baseline)...'; grid.gridConfig = { columns: [] }; grid.rows = []; await new Promise((r) => setTimeout(r, 50)); const renderTime = await measure(() => { grid.columns = baseColumns; grid.rows = [...baseRows]; }); allResults.push({ name: 'Baseline render', category: 'render', time: renderTime, unit: 'ms', target: scaledByCells(100), passed: renderTime < scaledByCells(100) }); updateResults(); // Render with features statusEl.textContent = 'Testing: Initial render (with features)...'; grid.gridConfig = { columns: [] }; grid.rows = []; await new Promise((r) => setTimeout(r, 50)); const pluginColumns = baseColumns.map((col, i) => ({ ...col, sticky: i === 0 ? ('left' as const) : undefined })); const pluginRenderTime = await measure(() => { grid.gridConfig = { columns: pluginColumns as ColumnConfig[], fitMode: 'fixed', features: { selection: 'range', pinnedColumns: true, multiSort: { maxSortColumns: 3 }, filtering: { debounceMs: 0 }, columnVirtualization: { threshold: 20, overscan: 3 }, }, }; grid.rows = [...baseRows]; }); allResults.push({ name: 'Render with 5 features', category: 'render', time: pluginRenderTime, unit: 'ms', target: scaledByCells(150), passed: pluginRenderTime < scaledByCells(150) }); updateResults(); // Warm render statusEl.textContent = 'Testing: Warm render (second render)...'; const warmRows = generateRows(rowCount, colCount); const warmRenderTime = await measure(() => { grid.rows = warmRows; }); allResults.push({ name: 'Warm render (data swap)', category: 'render', time: warmRenderTime, unit: 'ms', target: scaledByRows(50), passed: warmRenderTime < scaledByRows(50), note: warmRenderTime < pluginRenderTime ? 'Faster than cold ✓' : 'Slower than cold ⚠' }); updateResults(); // Time to interactive statusEl.textContent = 'Testing: Time to interactive...'; grid.gridConfig = { columns: [] }; grid.rows = []; await new Promise((r) => setTimeout(r, 50)); const ttiStart = performance.now(); grid.gridConfig = { columns: pluginColumns as ColumnConfig[], fitMode: 'fixed', features: { selection: 'range', pinnedColumns: true, multiSort: { maxSortColumns: 3 }, filtering: { debounceMs: 0 }, columnVirtualization: { threshold: 20, overscan: 3 }, }, }; grid.rows = [...baseRows]; await nextFrame(); const headerCell = grid.querySelector('[role="columnheader"]') as HTMLElement | null; if (headerCell) { headerCell.click(); await nextFrame(); } const ttiTime = performance.now() - ttiStart; allResults.push({ name: 'Time to interactive', category: 'render', time: ttiTime, unit: 'ms', target: scaledByCells(200), passed: ttiTime < scaledByCells(200) }); updateResults(); // 📜 SCROLL PERFORMANCE statusEl.textContent = 'Testing: Scroll performance...'; await new Promise((r) => setTimeout(r, 100)); const scrollContainer = grid.querySelector('.faux-vscroll'); if (scrollContainer) { const totalHeight = scrollContainer.scrollHeight; const viewportHeight = scrollContainer.clientHeight; const steps = 30; const stepSize = (totalHeight - viewportHeight) / steps; if (stepSize > 0) { // Warm-up for (let i = 0; i <= steps; i++) { scrollContainer.scrollTop = i * stepSize; await nextFrame(); } scrollContainer.scrollTop = 0; await new Promise((r) => setTimeout(r, 50)); // Measure const frameTimes: number[] = []; for (let i = 0; i <= steps; i++) { const start = performance.now(); scrollContainer.scrollTop = i * stepSize; await nextFrame(); frameTimes.push(performance.now() - start); } const avgScroll = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length; const p95Scroll = [...frameTimes].sort((a, b) => a - b)[Math.floor(frameTimes.length * 0.95)]; const p99Scroll = [...frameTimes].sort((a, b) => a - b)[Math.floor(frameTimes.length * 0.99)]; const scrollTarget = Math.min(33.33, 17 * (1 + (colCount - 10) / 40)); allResults.push({ name: 'Scroll avg frame', category: 'scroll', time: avgScroll, unit: 'ms', target: scrollTarget, passed: avgScroll < scrollTarget }); allResults.push({ name: 'Scroll P95 frame', category: 'scroll', time: p95Scroll, unit: 'ms', target: 33.33, passed: p95Scroll < 33.33 }); allResults.push({ name: 'Scroll P99 frame', category: 'scroll', time: p99Scroll, unit: 'ms', target: 50, passed: p99Scroll < 50 }); updateResults(); // Stress scroll statusEl.textContent = 'Testing: Stress scroll (random jumps)...'; scrollContainer.scrollTop = 0; await new Promise((r) => setTimeout(r, 50)); const stressJumps = 20; const stressFrameTimes: number[] = []; for (let i = 0; i < stressJumps; i++) { const randomPos = Math.random() * (totalHeight - viewportHeight); const start = performance.now(); scrollContainer.scrollTop = randomPos; await nextFrame(); stressFrameTimes.push(performance.now() - start); } const avgStressScroll = stressFrameTimes.reduce((a, b) => a + b, 0) / stressFrameTimes.length; const maxStressScroll = Math.max(...stressFrameTimes); allResults.push({ name: 'Stress scroll avg', category: 'scroll', time: avgStressScroll, unit: 'ms', target: 50, passed: avgStressScroll < 50 }); allResults.push({ name: 'Stress scroll max', category: 'scroll', time: maxStressScroll, unit: 'ms', target: 100, passed: maxStressScroll < 100 }); updateResults(); scrollContainer.scrollTop = 0; await new Promise((r) => setTimeout(r, 50)); } } // Horizontal scroll statusEl.textContent = 'Testing: Horizontal scroll performance...'; await new Promise((r) => setTimeout(r, 100)); const hScrollContainer = grid.querySelector('.tbw-scroll-area'); const colVirtPlugin = (grid as any).getPluginByName?.('columnVirtualization'); if (hScrollContainer && colCount >= 20) { const totalWidth = hScrollContainer.scrollWidth; const viewportWidth = hScrollContainer.clientWidth; const hSteps = 30; const hStepSize = (totalWidth - viewportWidth) / hSteps; if (hStepSize > 0) { for (let i = 0; i <= hSteps; i++) { hScrollContainer.scrollLeft = i * hStepSize; await nextFrame(); } hScrollContainer.scrollLeft = 0; await new Promise((r) => setTimeout(r, 50)); const hFrameTimes: number[] = []; for (let i = 0; i <= hSteps; i++) { const start = performance.now(); hScrollContainer.scrollLeft = i * hStepSize; await nextFrame(); hFrameTimes.push(performance.now() - start); } const avgHScroll = hFrameTimes.reduce((a, b) => a + b, 0) / hFrameTimes.length; const p95HScroll = [...hFrameTimes].sort((a, b) => a - b)[Math.floor(hFrameTimes.length * 0.95)]; allResults.push({ name: 'H-Scroll avg frame', category: 'scroll', time: avgHScroll, unit: 'ms', target: 50, passed: avgHScroll < 50 }); allResults.push({ name: 'H-Scroll P95 frame', category: 'scroll', time: p95HScroll, unit: 'ms', target: 66.67, passed: p95HScroll < 66.67 }); if (colVirtPlugin) { const isVirt = colVirtPlugin.getIsVirtualized(); const range = colVirtPlugin.getVisibleColumnRange(); allResults.push({ name: `Col virtualization (${range.end - range.start + 1}/${colCount} visible)`, category: 'scroll', time: isVirt ? 1 : 0, unit: 'bool', target: 1, passed: isVirt }); } updateResults(); } } // ⚡ OPERATIONS // Sort statusEl.textContent = 'Testing: Sort operation...'; if (scrollContainer) scrollContainer.scrollTop = 0; await new Promise((r) => setTimeout(r, 50)); const sortPlugin = (grid as any).getPluginByName?.('multiSort') as { setSortModel: (m: unknown[]) => void; clearSort: () => void } | undefined; if (sortPlugin) { const sortTarget = Math.round(150 * (Math.log2(rowCount) / Math.log2(10000))); const sortTime = await measure(() => { sortPlugin.setSortModel([{ field: 'id', direction: 'asc' }]); }); allResults.push({ name: 'Sort (ascending)', category: 'operation', time: sortTime, unit: 'ms', target: sortTarget, passed: sortTime < sortTarget }); updateResults(); grid.rows = generateRows(rowCount, colCount); await nextFrame(); await new Promise((r) => setTimeout(r, 50)); const reverseSortTime = await measure(() => { sortPlugin.setSortModel([{ field: 'id', direction: 'desc' }]); }); allResults.push({ name: 'Sort (descending)', category: 'operation', time: reverseSortTime, unit: 'ms', target: sortTarget, passed: reverseSortTime < sortTarget }); updateResults(); sortPlugin.clearSort(); await nextFrame(); } // Filter statusEl.textContent = 'Testing: Filter operation...'; const filterPlugin = (grid as any).getPluginByName?.('filtering') as { setFilter: (f: string, v: unknown) => void } | undefined; if (filterPlugin) { const filterTime = await measure(() => { filterPlugin.setFilter('col1', { type: 'text', operator: 'contains', value: 'R1' }); }); allResults.push({ name: 'Filter (to ~10%)', category: 'operation', time: filterTime, unit: 'ms', target: scaledByRows(50), passed: filterTime < scaledByRows(50) }); updateResults(); const clearFilterTime = await measure(() => { filterPlugin.setFilter('col1', null); }); allResults.push({ name: 'Clear filter', category: 'operation', time: clearFilterTime, unit: 'ms', target: scaledByRows(50), passed: clearFilterTime < scaledByRows(50) }); updateResults(); } // Data replacement statusEl.textContent = 'Testing: Data replacement...'; const newRows = generateRows(rowCount, colCount); const replaceTime = await measure(() => { grid.rows = newRows; }); allResults.push({ name: 'Full data replace', category: 'operation', time: replaceTime, unit: 'ms', target: scaledByRows(50), passed: replaceTime < scaledByRows(50) }); updateResults(); // Selection statusEl.textContent = 'Testing: Selection...'; const selectionPlugin = (grid as any).getPluginByName?.('selection') as { setRanges: (r: unknown[]) => void; clearSelection: () => void } | undefined; if (selectionPlugin) { const selectAllTime = await measure(() => { selectionPlugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: rowCount - 1, col: colCount - 1 } }]); }); allResults.push({ name: 'Select all cells', category: 'operation', time: selectAllTime, unit: 'ms', target: 50, passed: selectAllTime < 50 }); updateResults(); const clearSelectionTime = await measure(() => { selectionPlugin.clearSelection(); }); allResults.push({ name: 'Clear selection', category: 'operation', time: clearSelectionTime, unit: 'ms', target: 50, passed: clearSelectionTime < 50 }); updateResults(); } // Rapid data updates statusEl.textContent = 'Testing: Rapid data updates...'; const updateCount = 10; const updateTimes: number[] = []; for (let i = 0; i < updateCount; i++) { const modifiedRows = [...grid.rows] as Record<string, unknown>[]; const updateSize = Math.max(1, Math.floor(rowCount * 0.01)); for (let j = 0; j < updateSize; j++) { const idx = (i * updateSize + j) % rowCount; modifiedRows[idx] = { ...modifiedRows[idx], col1: `Updated-${i}-${j}` }; } const t = await measure(() => { grid.rows = modifiedRows; }); updateTimes.push(t); } const avgUpdateTime = updateTimes.reduce((a, b) => a + b, 0) / updateTimes.length; allResults.push({ name: `Rapid update (${updateCount}× 1%)`, category: 'operation', time: avgUpdateTime, unit: 'ms', target: scaledByRows(30), passed: avgUpdateTime < scaledByRows(30) }); updateResults(); // Column config change statusEl.textContent = 'Testing: Column config change...'; const configChangeTime = await measure(() => { const newColumns = baseColumns.map((col, i) => ({ ...col, width: i === 1 ? 200 : col.width })); grid.columns = newColumns; }); allResults.push({ name: 'Column config change', category: 'operation', time: configChangeTime, unit: 'ms', target: scaledByCells(50), passed: configChangeTime < scaledByCells(50) }); updateResults(); // Column resize statusEl.textContent = 'Testing: Column resize performance...'; const resizeFrameTimes: number[] = []; for (let i = 0; i < 20; i++) { const width = 80 + (i % 2 === 0 ? 40 : -20); const start = performance.now(); grid.columns = baseColumns.map((col, idx) => ({ ...col, width: idx === 1 ? width : col.width })); await nextFrame(); resizeFrameTimes.push(performance.now() - start); } const avgResize = resizeFrameTimes.reduce((a, b) => a + b, 0) / resizeFrameTimes.length; const p95Resize = [...resizeFrameTimes].sort((a, b) => a - b)[Math.floor(resizeFrameTimes.length * 0.95)]; allResults.push({ name: 'Resize avg frame', category: 'operation', time: avgResize, unit: 'ms', target: 33.33, passed: avgResize < 33.33 }); allResults.push({ name: 'Resize P95 frame', category: 'operation', time: p95Resize, unit: 'ms', target: 50, passed: p95Resize < 50 }); updateResults(); // 💾 MEMORY statusEl.textContent = 'Calculating: Memory & DOM metrics...'; const estimatedBytesPerRow = 40 + colCount * 52; const estimatedDataSize = rowCount * estimatedBytesPerRow; allResults.push({ name: 'Est. data size', category: 'memory', time: estimatedDataSize, unit: 'bytes', target: 500 * 1024 * 1024, passed: estimatedDataSize < 500 * 1024 * 1024 }); allResults.push({ name: 'Est. bytes/row', category: 'memory', time: estimatedBytesPerRow, unit: 'bytes', target: 5000, passed: estimatedBytesPerRow < 5000 }); const rowElements = grid.querySelectorAll('.data-grid-row'); const renderedRowCount = rowElements?.length ?? 0; const maxExpectedRows = Math.min(rowCount, 100); allResults.push({ name: `DOM rows (${renderedRowCount}/${rowCount.toLocaleString()})`, category: 'memory', time: renderedRowCount, unit: 'count', target: maxExpectedRows, passed: renderedRowCount <= maxExpectedRows, note: renderedRowCount <= maxExpectedRows ? 'Virtualization active' : 'Too many DOM nodes!' }); const cellElements = grid.querySelectorAll('.cell'); const renderedCellCount = cellElements?.length ?? 0; const expectedCellsPerRow = colCount <= 20 ? colCount : Math.min(colCount, 30); const maxExpectedCells = maxExpectedRows * expectedCellsPerRow; allResults.push({ name: 'DOM cells rendered', category: 'memory', time: renderedCellCount, unit: 'count', target: maxExpectedCells, passed: renderedCellCount <= maxExpectedCells, note: `${renderedCellCount.toLocaleString()} cells in DOM` }); updateResults(); // COMPLETE statusEl.textContent = 'Benchmark complete!'; runBtn.disabled = false; resultsEl.innerHTML = renderResultsTable(allResults, true); console.table(allResults.map((r) => ({ Benchmark: r.name, Result: r.unit === 'bytes' ? formatBytes(r.time) : r.unit === 'count' ? r.time.toLocaleString() : r.unit === 'bool' ? (r.time ? 'Yes' : 'No') : r.unit === 'info' ? (r.note || '—') : formatTime(r.time), Target: r.unit === 'bytes' ? '< ' + formatBytes(r.target) : r.unit === 'count' || r.unit === 'bool' || r.unit === 'info' ? (r.note || '—') : '< ' + formatTime(r.target), Status: r.passed ? '✅ PASS' : '❌ FAIL', }))); }); // #endregion ``` --- # Theming > Customize @toolbox-web/grid with CSS custom properties — colors, spacing, typography, dark mode, pre-built themes, and cascade layers. The `@toolbox-web/grid` component is fully themeable via CSS custom properties. Customize colors, spacing, typography, and more without modifying source code. ## Quick Start Override CSS custom properties on the grid element. Use the combined `[data-tbw-grid], tbw-grid` selector — it matches the canonical `<tbw-grid>` tag *and* version-suffixed tags (e.g. `<tbw-grid-v2-14-0>`) emitted when [multiple grid versions](https://toolboxjs.com/grid/guides/multi-version.md) share the page: ```css [data-tbw-grid], tbw-grid { --tbw-color-bg: #1a1a2e; --tbw-color-fg: #eaeaea; --tbw-color-border: #16213e; --tbw-color-header-bg: #0f3460; --tbw-color-row-hover: #1a1a4e; } ``` :::caution[Themes targeting only `tbw-grid` break in micro-frontends] If your theme might ever be loaded into a micro-frontend host where another bundle already owns `tbw-grid`, your bundle will register itself under a suffixed tag (e.g. `tbw-grid-v2-14-0`) and any rule keyed on the bare `tbw-grid` selector will silently miss those grids. **Always include `[data-tbw-grid]` in custom theme selectors** — every grid sets that attribute on its host regardless of the registered tag. The bundled themes (`@toolbox-web/grid/themes/dg-theme-*.css`) already follow this convention. See [Multi-version coexistence](https://toolboxjs.com/grid/guides/multi-version.md) for details. ::: ## Scaling with Font Size All sizing uses `em` units, so the grid scales proportionally with `font-size`: ```css tbw-grid { font-size: 1.25em; } /* 25% larger */ tbw-grid.compact { font-size: 0.875em; } /* Compact */ ``` ## Pre-built Themes Import a pre-built theme CSS file: ```typescript import '@toolbox-web/grid/themes/dg-theme-material.css'; ``` | Theme | Description | | --- | --- | | **Standard** | Polished, balanced look | | **Material** | Material Design 3 inspired | | **Bootstrap** | Bootstrap 5 styling | | **Vibrant** | Bold purple accents | | **Contrast** | High contrast for accessibility — [WCAG AAA](https://toolboxjs.com/grid/guides/accessibility.md#theme-contrast) text contrast | | **Large** | Larger fonts and spacing — [WCAG AAA](https://toolboxjs.com/grid/guides/accessibility.md#theme-contrast) text contrast | ## Dark Mode Support The grid uses `light-dark()` for automatic theme support: ```css tbw-grid { color-scheme: light dark; } /* Auto-adapt */ tbw-grid { color-scheme: dark; } /* Force dark */ ``` All built-in color variables use `light-dark()` internally, so they automatically provide appropriate colors for both modes. ## CSS Cascade Layers The grid organizes its styles into CSS cascade layers: ```css @layer tbw-base, tbw-plugins, tbw-theme; ``` Your unlayered CSS always wins — no `!important` needed. This makes theming predictable: - `tbw-base` — Core grid styles (structure, layout) - `tbw-plugins` — Plugin-contributed styles (selection highlighting, filter icons, etc.) - `tbw-theme` — Theme overrides (pre-built themes go here) - *(unlayered)* — Your custom CSS — always highest priority ## Interactive Theme Builder Use the theme builder below to customize every CSS variable in real-time — including core tokens and plugin-specific variables across all categories. The preview grid updates instantly as you tweak values. Export your theme as a `.css` file when you're done. ## CSS Variable Reference The tables below list **all 230 CSS custom properties** across core styles and every plugin, with their **live computed values** in this page. Toggle between light and dark mode to see how values change. ### Core Colors | Variable | Description | | --- | --- | | `--tbw-color-bg` | Grid background | | `--tbw-color-panel-bg` | Panel backgrounds | | `--tbw-color-fg` | Primary text color | | `--tbw-color-fg-muted` | Secondary text | | `--tbw-color-accent` | Accent color (focus, selection) | | `--tbw-color-accent-fg` | Text on accent | | `--tbw-color-accent-text` | Accent color for text (AA-contrast safe) | | `--tbw-color-success` | Success state | | `--tbw-color-warning` | Warning state | | `--tbw-color-error` | Error state | | `--tbw-color-shadow` | Box‑shadow color | ### Row & Cell Colors | Variable | Description | | --- | --- | | `--tbw-color-selection` | Selection background | | `--tbw-color-row-alt` | Alternating row color | | `--tbw-color-row-hover` | Row hover color | | `--tbw-color-active-row-bg` | Active row background | ### Header | Variable | Description | | --- | --- | | `--tbw-color-header-bg` | Header background | | `--tbw-color-header-fg` | Header text color | | `--tbw-color-header-separator` | Header column separator | | `--tbw-color-header-group-fg` | Column group header text | | `--tbw-header-height` | Minimum height of the header row. Presentation only — the grid derives the header allowance for scroll math from live DOM geometry, so this never affects virtualization. | | `--tbw-font-size-header` | Header font size | | `--tbw-font-weight-header` | Header font weight | | `--tbw-font-weight-header-group` | Column group font weight | | `--tbw-header-text-transform` | Header text transform | | `--tbw-header-letter-spacing` | Header letter spacing | | `--tbw-align-header` | Header content alignment | | `--tbw-align-header-group` | Column group alignment | ### Borders | Variable | Description | | --- | --- | | `--tbw-color-border` | Default border color | | `--tbw-color-border-strong` | Strong border color | | `--tbw-color-border-cell` | Cell border color | | `--tbw-color-border-header` | Header border color | | `--tbw-border-radius` | Border radius | | `--tbw-border-width` | Default border width | | `--tbw-border-style` | Default border style | | `--tbw-border-input` | Input border shorthand | | `--tbw-border-header` | Header border shorthand | | `--tbw-row-divider` | Row divider border | | `--tbw-row-hover-outline` | Row hover outline | | `--tbw-active-row-outline` | Active row outline | ### Typography | Variable | Description | | --- | --- | | `--tbw-font-family` | Font family | | `--tbw-font-size` | Base font size | | `--tbw-font-size-sm` | Small font size | | `--tbw-font-size-xs` | Extra small font size | | `--tbw-font-size-2xs` | Extra‑extra small font size | ### Spacing | Variable | Description | | --- | --- | | `--tbw-spacing-xs` | Extra small spacing | | `--tbw-spacing-sm` | Small spacing | | `--tbw-spacing-md` | Medium spacing | | `--tbw-spacing-lg` | Large spacing | | `--tbw-spacing-xl` | Extra large spacing | | `--tbw-cell-padding` | Cell padding | | `--tbw-cell-padding-v` | Cell vertical padding | | `--tbw-cell-padding-h` | Cell horizontal padding | | `--tbw-cell-padding-header` | Header cell padding | | `--tbw-cell-padding-input` | Input cell padding | ### Row & Cell Dimensions | Variable | Description | | --- | --- | | `--tbw-row-height` | Row height | | `--tbw-cell-white-space` | Cell text wrapping | | `--tbw-density-scale` | Density multiplier | ### Focus & Selection | Variable | Description | | --- | --- | | `--tbw-focus-outline-width` | Focus ring width | | `--tbw-focus-outline` | Focus ring style | | `--tbw-focus-outline-offset` | Focus ring offset | | `--tbw-focus-background` | Focused cell background tint | | `--tbw-range-border-color` | Range selection border | | `--tbw-range-selection-bg` | Range selection background | ### Icons | Variable | Description | | --- | --- | | `--tbw-base-icon-size` | Base icon size (bulk override) | | `--tbw-icon-size` | Icon size | | `--tbw-icon-size-sm` | Small icon size | | `--tbw-checkbox-size` | Checkbox size | | `--tbw-toggle-size` | Toggle icon size | ### Resize Handle | Variable | Description | | --- | --- | | `--tbw-resize-handle-width` | Resize handle width | | `--tbw-resize-handle-color` | Resize handle color | | `--tbw-resize-handle-color-hover` | Resize handle hover color | | `--tbw-resize-handle-border-radius` | Resize handle corner radius | | `--tbw-resize-indicator-width` | Resize indicator width | | `--tbw-resize-indicator-color` | Resize indicator color | | `--tbw-resize-indicator-opacity` | Resize indicator opacity | | `--tbw-resize-hit-area` | Resize handle clickable area | ### Animation | Variable | Description | | --- | --- | | `--tbw-transition-duration` | Transition duration | | `--tbw-transition-ease` | Transition easing | | `--tbw-animation-duration` | Animation duration | | `--tbw-animation-easing` | Animation easing | | `--tbw-animation-enabled` | Enable animations (0 to disable) | | `--tbw-row-change-duration` | Row change highlight duration | | `--tbw-row-insert-duration` | Row insert animation duration | | `--tbw-row-remove-duration` | Row remove animation duration | | `--tbw-row-change-color` | Row change highlight color | ### Sorting Indicators | Variable | Description | | --- | --- | | `--tbw-sort-indicator-color` | Sort indicator color | | `--tbw-sort-indicator-active-color` | Active sort indicator | | `--tbw-sort-indicator-display` | Sort indicator display | | `--tbw-sort-indicator-visibility` | Sort indicator visibility | ### Shell & Panels | Variable | Description | | --- | --- | | `--tbw-shell-header-height` | Shell header height | | `--tbw-shell-header-bg` | Shell header background | | `--tbw-shell-header-border` | Shell header border color | | `--tbw-shell-title-font-size` | Shell title font size | | `--tbw-shell-title-font-weight` | Shell title font weight | | `--tbw-tool-panel-width` | Tool panel width | | `--tbw-tool-panel-bg` | Tool panel background | | `--tbw-tool-panel-border` | Tool panel border color | | `--tbw-tool-panel-header-height` | Tool panel header height | | `--tbw-tool-panel-transition` | Tool panel open/close transition | | `--tbw-toolbar-button-size` | Toolbar button size | | `--tbw-toolbar-button-gap` | Toolbar button gap | ### Component Shortcuts | Variable | Description | | --- | --- | | `--tbw-panel-padding` | Panel content padding | | `--tbw-panel-gap` | Panel content gap | | `--tbw-menu-item-padding` | Menu item padding | | `--tbw-menu-item-gap` | Menu item gap | | `--tbw-menu-min-width` | Menu minimum width | | `--tbw-button-padding` | Button padding | | `--tbw-button-padding-sm` | Small button padding | | `--tbw-input-height` | Input height | | `--tbw-input-padding` | Input padding | | `--tbw-detail-padding` | Detail row padding | | `--tbw-detail-max-height` | Detail row max height | | `--tbw-indicator-size` | Indicator dot size | ### Scrollbar | Variable | Description | | --- | --- | | `--tbw-scrollbar-thumb` | Scrollbar thumb color | | `--tbw-scrollbar-track` | Scrollbar track color | ### Loading Spinner | Variable | Description | | --- | --- | | `--tbw-spinner-size` | Spinner size (grid-level) | | `--tbw-spinner-border-width` | Spinner border thickness | | `--tbw-spinner-color` | Spinner active color | | `--tbw-spinner-track-color` | Spinner track color | ### Context Menu (Plugin) | Variable | Description | | --- | --- | | `--tbw-context-menu-bg` | Context menu background | | `--tbw-context-menu-fg` | Context menu text color | | `--tbw-context-menu-border` | Context menu border color | | `--tbw-context-menu-hover` | Context menu item hover | | `--tbw-context-menu-muted` | Muted text (shortcuts) | | `--tbw-context-menu-danger` | Danger action color | | `--tbw-context-menu-radius` | Menu border radius | | `--tbw-context-menu-shadow` | Menu box shadow | | `--tbw-context-menu-min-width` | Minimum menu width | | `--tbw-context-menu-font-size` | Menu font size | | `--tbw-context-menu-font-family` | Menu font family | | `--tbw-context-menu-item-padding` | Menu item padding | | `--tbw-context-menu-item-gap` | Menu item gap | | `--tbw-context-menu-icon-size` | Menu icon size | | `--tbw-context-menu-shortcut-size` | Shortcut text size | | `--tbw-context-menu-arrow-size` | Submenu arrow size | ### Filtering (Plugin) | Variable | Description | | --- | --- | | `--tbw-filter-panel-bg` | Filter panel background | | `--tbw-filter-panel-fg` | Filter panel text color | | `--tbw-filter-panel-border` | Filter panel border | | `--tbw-filter-panel-radius` | Filter panel radius | | `--tbw-filter-panel-shadow` | Filter panel shadow color | | `--tbw-filter-accent` | Filter accent color | | `--tbw-filter-accent-fg` | Filter accent text color | | `--tbw-filter-hover` | Filter item hover | | `--tbw-filter-muted` | Filter muted text | | `--tbw-filter-divider` | Filter divider color | | `--tbw-filter-input-bg` | Filter input background | | `--tbw-filter-input-border` | Filter input border color | | `--tbw-filter-input-radius` | Filter input radius | | `--tbw-filter-item-height` | Filter list item height | | `--tbw-filter-search-padding` | Filter search input padding | | `--tbw-filter-btn-padding` | Filter button padding | | `--tbw-filter-btn-font-weight` | Filter button font weight | | `--tbw-filter-btn-min-height` | Filter button min height | | `--tbw-filter-btn-display` | Header filter button display | | `--tbw-filter-btn-visibility` | Header filter button visibility | ### Selection (Plugin) | Variable | Description | | --- | --- | | `--tbw-selection-border-style` | Selection border style | | `--tbw-selection-border-width` | Selection border width | | `--tbw-selection-warning-bg` | Invalid selection background | | `--tbw-selection-disabled-opacity` | Dimming applied to non-selectable rows/cells | ### Editing (Plugin) | Variable | Description | | --- | --- | | `--tbw-editing-bg` | Editing cell background | | `--tbw-editing-row-bg` | Editing row background | | `--tbw-editing-border` | Editor input border | | `--tbw-padding-editing-input` | Editor input padding | | `--tbw-font-size-editor` | Editor font size | | `--tbw-editing-row-outline-color` | Editing row outline color | | `--tbw-editing-row-outline-width` | Editing row outline width | | `--tbw-invalid-bg` | Invalid cell background | | `--tbw-invalid-border-color` | Invalid cell border color | ### Row Grouping (Plugin) | Variable | Description | | --- | --- | | `--tbw-grouping-rows-bg` | Group row background | | `--tbw-grouping-rows-bg-hover` | Group row hover bg | | `--tbw-grouping-rows-toggle-hover` | Toggle button hover | | `--tbw-grouping-rows-count-color` | Group count text color | | `--tbw-grouping-rows-aggregate-color` | Aggregate value color | | `--tbw-group-indent-width` | Group indent per level | ### Grouping Columns (Plugin) | Variable | Description | | --- | --- | | `--tbw-grouping-columns-header-bg` | Column group header bg | | `--tbw-grouping-columns-border` | Column group border | | `--tbw-grouping-columns-separator` | Column group separator | ### Tree (Plugin) | Variable | Description | | --- | --- | | `--tbw-tree-indent-width` | Tree indentation width | | `--tbw-tree-toggle-size` | Tree toggle icon size | | `--tbw-tree-accent` | Tree expand/collapse accent | ### Master-Detail (Plugin) | Variable | Description | | --- | --- | | `--tbw-master-detail-bg` | Detail row background | | `--tbw-master-detail-border` | Detail row border | ### Pivot (Plugin) | Variable | Description | | --- | --- | | `--tbw-pivot-group-bg` | Pivot group row bg | | `--tbw-pivot-group-hover` | Pivot group hover bg | | `--tbw-pivot-leaf-bg` | Pivot leaf row bg | | `--tbw-pivot-grand-total-bg` | Grand total row bg | | `--tbw-pivot-toggle-size` | Pivot toggle icon size | | `--tbw-pivot-toggle-color` | Pivot toggle color | | `--tbw-pivot-toggle-hover-bg` | Pivot toggle hover bg | | `--tbw-pivot-toggle-hover-color` | Pivot toggle hover color | | `--tbw-pivot-count-color` | Pivot count text color | | `--tbw-pivot-border` | Pivot section border | | `--tbw-pivot-section-bg` | Pivot configurator bg | | `--tbw-pivot-header-bg` | Pivot configurator header bg | | `--tbw-pivot-drop-border` | Drop zone border | | `--tbw-pivot-drop-bg` | Drop zone background | | `--tbw-pivot-drop-active` | Active drop zone bg | | `--tbw-pivot-chip-bg` | Pivot chip background | | `--tbw-pivot-chip-border` | Pivot chip border | | `--tbw-pivot-chip-hover` | Pivot chip hover bg | | `--tbw-pivot-chip-remove-hover-bg` | Chip remove button hover bg | | `--tbw-pivot-chip-remove-hover-fg` | Chip remove button hover text | ### Multi-Sort (Plugin) | Variable | Description | | --- | --- | | `--tbw-multi-sort-badge-bg` | Sort badge background | | `--tbw-multi-sort-badge-color` | Sort badge text color | | `--tbw-multi-sort-badge-size` | Sort badge size | ### Visibility Panel (Plugin) | Variable | Description | | --- | --- | | `--tbw-visibility-hover` | Visibility item hover | | `--tbw-visibility-indicator` | Visibility indicator | | `--tbw-visibility-border` | Visibility panel border | | `--tbw-visibility-btn-bg` | Visibility button bg | ### Pinned Rows (Plugin) | Variable | Description | | --- | --- | | `--tbw-pinned-rows-bg` | Pinned rows background | | `--tbw-pinned-rows-border` | Pinned rows border | | `--tbw-pinned-rows-color` | Pinned rows text color | | `--tbw-aggregation-bg` | Aggregation row background | | `--tbw-aggregation-border` | Aggregation row border | | `--tbw-aggregation-font-size` | Aggregation font size | | `--tbw-aggregation-font-weight` | Aggregation font weight | ### Responsive (Plugin) | Variable | Description | | --- | --- | | `--tbw-responsive-duration` | Card layout transition | ### Print (Plugin) | Variable | Description | | --- | --- | | `--tbw-print-border` | Print header border | | `--tbw-print-muted` | Print muted text | | `--tbw-print-cell-border` | Print cell border | ### Reorder Columns (Plugin) | Variable | Description | | --- | --- | | `--tbw-reorder-indicator` | Column reorder indicator | ### Reorder Rows (Plugin) | Variable | Description | | --- | --- | | `--tbw-row-reorder-handle-color` | Row drag handle color | | `--tbw-row-reorder-handle-hover` | Row drag handle hover color | | `--tbw-row-reorder-indicator` | Row reorder drop indicator | | `--tbw-row-reorder-moving-bg` | Moving row background | | `--tbw-row-reorder-moving-border` | Moving row border color | ### Z-Index Layers | Variable | Description | | --- | --- | | `--tbw-z-layer-rows` | Row area z-index | | `--tbw-z-layer-header` | Header z-index | | `--tbw-z-layer-pinned-rows` | Pinned rows z-index | | `--tbw-z-layer-toolpanel` | Tool panel z-index | :::caution[Accent is a surface token, not a text token] `--tbw-color-accent` is tuned for focus rings, borders and fills, where WCAG only requires 3:1. When you apply accent to **text**, use `--tbw-color-accent-text` instead — it is the AA-safe (4.5:1) counterpart. Overriding one does not change the other, so set both when you re-brand. ::: ## Building a Custom Theme Create a CSS file with your overrides: ```css /* my-theme.css */ @layer tbw-theme { [data-tbw-grid], tbw-grid { --tbw-color-bg: #fafafa; --tbw-color-fg: #333; --tbw-color-border: #e0e0e0; --tbw-color-header-bg: #f5f5f5; --tbw-color-header-fg: #555; --tbw-color-row-hover: #e3f2fd; --tbw-color-accent: #1976d2; --tbw-color-focus-ring: #1976d2; --tbw-cell-padding: 0.4em 0.75em; --tbw-font-family: 'Inter', sans-serif; } } ``` Import it **after** the grid: ```typescript import '@toolbox-web/grid'; import './my-theme.css'; ``` ## Programmatic Styles Since the grid uses **light DOM**, standard CSS works — global stylesheets, `<style>` in `<head>`, or external CSS files can all target elements inside `<tbw-grid>`. For styles you need to inject or update **from JavaScript at runtime**, use the `registerStyles()` API: ```typescript // Inject styles programmatically grid.registerStyles('my-highlights', ` .row-warning { background: #fff3e0; } .row-error { background: #fce4ec; } `); // Remove when no longer needed grid.unregisterStyles('my-highlights'); ``` :::caution Do not place `<style>` elements as **children of `<tbw-grid>`** — the grid calls `replaceChildren()` during renders, which removes child nodes. This only applies to styles placed *inside* the grid element itself; external CSS is unaffected. ::: ## Plugin Styles Plugins inject their own CSS via `adoptedStyleSheets` in the `tbw-plugins` cascade layer. You can override any plugin class in your theme: | Plugin | Key CSS Classes | What They Style | |--------|----------------|----------------| | **Selection** | `.tbw-cell-selected`, `.tbw-row-selected` | Selected cells and rows highlight | | **Editing** | `.tbw-cell-editing`, `.tbw-row-dirty`, `.tbw-row-new` | Active editor and dirty tracking indicators | | **Filtering** | `.tbw-filter-active` | Filter icon active state | | **Grouping** | `.tbw-group-row`, `.tbw-group-toggle`, `.tbw-row-expanded` | Group header rows, expand/collapse icons, expanded group | | **Tree** | `.tbw-row-expanded` | Parent rows whose children are visible | | **Master/Detail** | `.tbw-row-expanded` | Master rows whose detail panel is showing | | **Responsive** | `.tbw-card-view`, `.tbw-card-row` | Card layout mode styling | :::tip[Style expanded rows with `.tbw-row-expanded`, not `[aria-expanded]`] Tree, GroupingRows, and MasterDetail all toggle `.tbw-row-expanded` on the row element when it is in an expanded state. Use this class for theming. The `aria-expanded` attribute is reserved for assistive technology and may move (MasterDetail puts it on the toggle button, not the row), so styling against `[aria-expanded="true"]` is fragile and framework-specific. ::: Override in your theme: ```css @layer tbw-theme { tbw-grid .tbw-cell-selected { background: rgba(25, 118, 210, 0.15); } tbw-grid .tbw-row-dirty { border-left: 3px solid orange; } tbw-grid .tbw-row-expanded { background: rgba(25, 118, 210, 0.05); } } ``` ## Icon Customization The grid uses a **CSS-first hybrid icon system** with two configuration paths: | Approach | Best for | How it works | |----------|----------|-------------| | **CSS variables** (default) | Themes, static icons, no-JS customization | Override `--tbw-icon-*` properties on `tbw-grid` | | **JavaScript** (`gridConfig.icons`) | Dynamic icons, icon libraries, runtime changes | Set `icons` in grid config — takes precedence over CSS | CSS is the **recommended default** — it requires no JavaScript, works with cascade layers, and is tree-shakeable. Use the JS path when you need dynamic icons (e.g., loading from an icon library at runtime) or `HTMLElement` instances. For the full list of icon keys with their TypeScript signatures, see the [`GridIcons` interface](https://toolboxjs.com/grid/api/core/interfaces/gridicons.md). ### JavaScript Path (`gridConfig.icons`) For programmatic control (e.g., dynamic icon libraries, `HTMLElement` icons, or runtime theme switching), use `gridConfig.icons`. JavaScript values take precedence over CSS variables: ```typescript grid.gridConfig = { icons: { sortAsc: '↑', sortDesc: '↓', expand: '+', collapse: '−', // Values may also be HTMLElement instances for fully custom markup }, }; ``` All grid icons (sort indicators, expand/collapse, filter, drag handles, etc.) are rendered via CSS custom properties. The remainder of this section covers the CSS-first approach — override them in your theme. ### Text / Emoji Icons Set the `--tbw-icon-<key>` variable to any CSS `<string>` value: ```css @layer tbw-theme { tbw-grid { --tbw-icon-sort-asc: '↑'; --tbw-icon-sort-desc: '↓'; --tbw-icon-expand: '+'; --tbw-icon-collapse: '−'; --tbw-icon-size: 1.2em; } } ``` ### SVG Mask Icons The **filter** and **filter-active** icons use SVG masks by default — just override their `-mask` variable: ```css @layer tbw-theme { tbw-grid { --tbw-icon-filter: ''; --tbw-icon-filter-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z'/%3E%3C/svg%3E"); } } ``` For **any other icon**, switching to mask mode requires setting the content to `''`, providing a `-mask` URL, and adding `background: currentColor` with explicit dimensions: ```css @layer tbw-theme { tbw-grid { /* Set content to '' and provide the mask URL */ --tbw-icon-sort-asc: ''; --tbw-icon-sort-asc-mask: url("data:image/svg+xml,..."); } /* Add background + dimensions so the mask is visible (cannot be shipped generically because they would affect text/emoji icon layout) */ tbw-grid [data-icon='sort-asc']:empty::before { width: var(--tbw-icon-size, 1em); height: var(--tbw-icon-size, 1em); background: currentColor; } } ``` The grid ships `mask-image`, `mask-size`, `mask-repeat`, and `mask-position` for every icon — you only need to add `background: currentColor`. ### Available Variables | Variable | Default | Description | |----------|---------|-------------| | `--tbw-icon-expand` | `'▶'` | Tree / group / master-detail expand | | `--tbw-icon-collapse` | `'▼'` | Tree / group / master-detail collapse | | `--tbw-icon-sort-asc` | `'▲'` | Sort ascending indicator | | `--tbw-icon-sort-desc` | `'▼'` | Sort descending indicator | | `--tbw-icon-sort-none` | `'⇅'` | Unsorted indicator | | `--tbw-icon-filter` | `''` (mask) | Column filter icon | | `--tbw-icon-filter-active` | `''` (mask) | Active filter icon | | `--tbw-icon-submenu-arrow` | `'▶'` | Context menu submenu arrow | | `--tbw-icon-drag-handle` | `'⋮⋮'` | Row drag handle | | `--tbw-icon-tool-panel` | `'☰'` | Tool panel toggle | | `--tbw-icon-print` | `'🖨️'` | Print button | Each icon also has a `-mask` variant (e.g., `--tbw-icon-sort-asc-mask`) for SVG mask-based rendering with `currentColor`. ## Touch & Pointer Input Interactive controls meet a minimum hit-target size governed by `--tbw-touch-target-min` — on a mouse just as much as on a touchscreen. The `24px` default satisfies [WCAG 2.2 SC 2.5.8 Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html), Level **AA**. Raise it to `44px` to also satisfy [SC 2.5.5 Target Size (Enhanced)](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html), Level **AAA**: ```css tbw-grid { --tbw-touch-target-min: 44px; --tbw-tool-panel-resize-overhang: 22px; } ``` | Token | Default | Description | |-------|---------|-------------| | `--tbw-touch-target-min` | `24px` | Minimum pointer target for interactive controls. Affects filter buttons, drag handles, resize handles, expand toggles, selection checkboxes. Small controls grow a transparent hit area rather than their box, so density is unchanged. | | `--tbw-tool-panel-resize-overhang` | `12px` | Half the tool-panel splitter's target width. Kept separate because browsers reject a calculated `overflow-clip-margin`; raise it alongside `--tbw-touch-target-min`. | Controls that are only visible on hover (filter button, drag handles) are kept visible at all times under `@media (hover: none)` so touch users can always reach them without a preceding hover event. Fine-pointer rendering is pixel-identical to before — all changes are scoped to coarse-pointer or hover-none media queries. --- # Touch Input > How @toolbox-web/grid works on touch devices — gesture map, long-press interactions, hit-target sizing, and known browser quirks. The grid is **touch-friendly by default**. Native two-finger scrolling, tap-to-focus, tap-header-to-sort, and drag-resize all work on touch screens without any configuration. This page explains how the grid maps gestures to features and which interactions require plugins. For hit-target sizing, keyboard equivalents and the rest of the accessibility surface, see [Accessibility](https://toolboxjs.com/grid/guides/accessibility.md). ## Overview `@toolbox-web/grid` handles touch via the Web Pointer Events API (`pointerdown` / `pointermove` / `pointerup`) combined with `setPointerCapture`. This approach is robust under DOM virtualization — because virtualization recycles cell elements mid-gesture, touch events lose their target; pointer capture routes all events to the grid host element regardless of DOM changes. Capture is taken the moment a press turns into a drag rather than on the press itself, so a plain tap or click still reaches the cell underneath it. The grid renders into **light DOM** with a faux scrollbar pattern: a zero-opacity `div.faux-vscroll` acts as the scroll container, while the visible row area clips and translates. Touch scrolling drives this faux scrollbar via JS, which is why all scrollable elements carry `touch-action: none` (see [Known browser quirks](#known-browser-quirks)). ## Gesture map The table below shows how each action maps across input types. | Action | Mouse | Trackpad | Touch | |---|---|---|---| | Focus a cell | Click cell | Click / tap | Tap cell | | Sort by column | Click header | Click header | Tap header | | Multi-sort (secondary key) | Shift+click header | Shift+click | See note below ↓ | | Scroll rows (vertical) | Scroll wheel / drag scrollbar | Two-finger swipe | One-finger swipe | | Scroll columns (horizontal) | Scroll wheel / drag scrollbar | Two-finger swipe | One-finger swipe | | Resize column | Drag resize handle | Drag resize handle | Drag resize handle | | Reorder column | Drag header | Drag header | Drag header | | Toggle row selection (single) | Click row | Click row | Tap row | | Toggle row multi-select | Ctrl+click row | Ctrl+click | See note below ↓ | | Range-select rows | Shift+click row | Shift+click | See note below ↓ | | Range-select cells | Ctrl+click / drag | Ctrl+click / drag | See note below ↓ | | Open context menu | Right-click | Two-finger tap | Long-press | | Header menu | — | — | Long-press header | | Resize a tool panel | Drag splitter | Drag splitter | Drag splitter | | Paint a cell range | Drag across cells | Drag across cells | Long-press cell, then drag | All drag interactions in the table above are driven by [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) with [pointer capture](https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture), so mouse, pen and touch follow one code path — there is no separate touch implementation to fall out of sync. Cell-range painting is the one gesture that behaves differently by modality: with a fine pointer (mouse, trackpad) the drag starts as soon as the pointer moves a few pixels, while with a coarse pointer (touch, pen) it requires a 400 ms long-press first, so that a plain swipe still scrolls the grid. - **Multi-sort via touch** — secondary header sort will be accessible via the column header menu (long-press header → header menu → Sort → Add sort key). - **Multi-select via touch** — long-pressing a row enters *selection mode* (a toolbar appears). Subsequent taps toggle rows in that mode. Described further in [Selection-mode UX](#selection-mode-ux). - **Range-select rows via touch** — long-pressing a second row while in selection mode extends the range from the anchor. - **Range-select cells via touch** — long-press a cell, then drag to paint the range. Its corners can then be dragged with the range handles. - **Long-press cell / row (no SelectionPlugin)** — opens the context menu, exactly as right-click does. ## Long-press priority order When a long-press occurs, the grid resolves the action according to the following priority chain: 1. **Header long-press → Column header menu** (highest priority) — opens the column header menu regardless of which plugins are active. 2. **Row long-press + `SelectionPlugin` in `mode: 'row'` → Selection mode** — enters touch selection mode; a toolbar appears at the top of the grid. 3. **Cell long-press + `SelectionPlugin` in `mode: 'cell'` / `'range'` → Range painting** — drag to paint the range. 4. **Row / cell long-press, nothing above applies → Context menu** — falls back to `ContextMenuPlugin` if it is registered. This order is the agreed policy for the touch-input epic and applies to all future long-press handlers. ### How the fall-through works There is no long-press polyfill for the context menu: browsers already synthesise a native `contextmenu` from a touch long-press, just as they do from a right-click. The grid instead resolves the conflict from the *other* direction — after 400 ms it offers the press to the plugins, and **only if one claims it** does it suppress the browser's `contextmenu` for a short window. When nothing claims the press, no suppression happens and the menu opens on its own. Two consequences worth knowing: - Long-press works with `ContextMenuPlugin` alone — no `SelectionPlugin` required, and no configuration. - When selection mode *has* claimed the press, the context-menu items are still one tap away via the **More…** button in the selection toolbar, so touch users never lose right-click parity. :::note[No column header menu yet] There is no column header menu, so step 1 never fires: a header long-press falls through to the context menu with `isHeader: true`. Multi-sort and "Select column" are therefore not yet reachable by touch. ::: ## Selection-mode UX When a user long-presses a row on a touch device and `SelectionPlugin` is active in `mode: 'row'`, the grid enters **selection mode**: - A toolbar appears at the top of the grid showing: - **N selected** — count badge - **Select all** — selects all rows in the current data set - **Clear** — deselects all - **More…** — surfaces the `ContextMenuPlugin` items, so touch users keep parity with right-click. Hidden when no `ContextMenuPlugin` is registered. - **Done** — exits selection mode - Tapping a row toggles its selection — no modifier key needed. - Long-pressing a second row extends the selection from the long-press anchor. - Tapping **Done**, or pressing `Escape` on an external keyboard, exits selection mode. Selection mode is **additive**. Mouse users keep `Ctrl+click` and `Shift+click` exactly as before, and the mode is entered from the *event's* `pointerType` rather than the `(pointer: coarse)` media query — so on a hybrid device such as a Surface, a finger gets selection mode while the mouse keeps its chords. ### `touchMode` `SelectionConfig.touchMode` controls what happens to the selection when the mode is exited: | Value | Behaviour | |---|---| | `'transient'` (default) | Exiting clears the selection. The mode *is* the selection, as in Gmail. | | `'sticky'` | The selection survives, so a later round can build on it. | ```ts new SelectionPlugin({ mode: 'row', touchMode: 'sticky' }); ``` Read the current state with the `touchSelectionActive` getter, and exit programmatically with `exitTouchSelection()`. ### Cell range corner handles When a cell range is started by a **finger or stylus** in `mode: 'range'`, two draggable dots are rendered at the range's top-left and bottom-right corners (the iOS Numbers / Google Sheets idiom). Dragging one resizes the range. They are deliberately **touch-only**: touch has no Shift/Ctrl, so the handles are the only way to adjust a range. A mouse-started range never shows them — desktop users adjust with Shift+click and can hold multiple ranges at once, where handles sitting inside another range would only be in the way. Because the grid virtualizes rows, a handle whose anchor cell has scrolled out of the rendered window is hidden until that cell returns. ## Hit-target sizing Every interactive element in the grid answers a pointer across at least 24 × 24 CSS pixels, which is WCAG 2.2 SC 2.5.8 Target Size (Minimum). Because the criterion is not conditional on pointer type, this is an accessibility guarantee rather than a touch feature, and it is documented with the rest of them: see [Target size](https://toolboxjs.com/grid/guides/accessibility.md#target-size) for how the grid reaches the minimum without giving up density, and for the `--tbw-touch-target-min` and `--tbw-tool-panel-resize-overhang` tokens that raise it. What is specific to touch is that a coarse pointer also enlarges the *visible* box of several controls. That is a comfort choice on top of the target minimum, not the thing that satisfies it. ## Known browser quirks ### `touch-action: none` policy The grid sets `touch-action: none` on all JS-managed scroll and interaction elements (`.tbw-grid-content`, `.rows-viewport`, `.faux-vscroll`, `.resize-handle`). This is intentional and correct: the grid's faux-scrollbar pattern drives scrolling via JS pointer-capture events, and the browser compositor must not compete with or cancel those handlers mid-gesture. **Why not `pan-x pan-y`?** That value delegates scrolling to the browser compositor, which bypasses the faux scrollbar entirely — the grid position would update but the custom scrollbar would not. **Why not `manipulation`?** That value suppresses double-tap-to-zoom, which is a [WCAG 1.4.4 violation](https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html) and breaks pinch-zoom on pages where the grid is embedded. ### Pointer capture and DOM virtualization Without `setPointerCapture`, a touch-scroll gesture over a grid would stop after ~2 rows because DOM virtualization recycles the original touch-target element. The grid uses `setPointerCapture` to route all pointer events to `.tbw-grid-content` regardless of DOM changes. If your environment disables `setPointerCapture` (some test harnesses or sandboxed iframes), touch scrolling may stop mid-gesture. ### iOS Safari scroll momentum iOS Safari applies its own scroll momentum to elements with `overflow: auto`. Because the grid uses a faux scrollbar (not native overflow), momentum scrolling is implemented in JS (`touch-scroll.ts`). The momentum curve matches the CSS `ease-out` curve and stops naturally at content boundaries. --- # Troubleshooting > Solutions to common issues when working with @toolbox-web/grid — height and virtualization problems, performance tuning, plugin conflicts, and framework adapter development. The grid is designed to work out of the box with minimal setup. If you're hitting issues with basic configuration — TypeScript types, framework imports, or event handling — check the [Getting Started](https://toolboxjs.com/grid/getting-started.md) guide first, which covers setup for all frameworks. This page covers two things: **how to debug the grid when something is wrong**, and the genuine gotchas that can't be solved in code alone. --- ## How to debug the grid When the grid isn't doing what you expect, work through this checklist in order. Most issues fall out at one of the first three steps. ### 1. Read the console — every warning has a code Every user-facing warning and error from the grid is prefixed with `[tbw-grid#<your-id>:<PluginName>]` and tagged with a stable diagnostic code (e.g. `TBW001`, `TBW020`). The code → cause mapping is published on the [Error Codes](https://toolboxjs.com/grid/errors.md) reference page. ``` [tbw-grid#employees:SelectionPlugin] TBW020 SelectionPlugin requires EditingPlugin when `editable` is set on any column. Add to your features: import '@toolbox-web/grid/features/editing'; ``` What the parts mean: | Part | Meaning | |---|---| | `[tbw-grid…]` | Came from the grid, not your app code | | `#employees` | The grid's `id` attribute (or omitted if anonymous) — useful when you have several on a page | | `:SelectionPlugin` | The plugin that produced the message (omitted for core messages) | | `TBW020` | Stable code — links to [the errors page](https://toolboxjs.com/grid/errors.md) | If a TBW code appears in production logs or a Sentry event, search the errors page first — most have an actionable fix one click away. ### 2. Inspect the grid's effective configuration The biggest source of "why isn't this working?" confusion is *what config did the grid actually end up with*, after defaults, features, plugin merges, and light-DOM column nodes are all combined. The grid exposes the resolved config two ways: ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('#employees'); // typed DataGridElement<T> — no casts needed await grid.ready(); // wait for first render // Public — frozen, async, safe to assign from production code const config = await grid.getConfig(); console.log(config); console.log(config.plugins); // Internal — synchronous getter, the live merged config. Great in the DevTools // console; do NOT call from production code (see stability note below). console.log(grid.effectiveConfig); console.log(grid.effectiveConfig.plugins); // Always-public state console.log(grid.columns); // current column array (post-plugin transforms) console.log(grid.rows); // current rows (post-sort/filter) console.log(grid.sourceRows); // unfiltered, unsorted source data ``` :::caution[Stability of internal APIs] Anything documented below as **internal** (also: any property or method starting with `_` or `#` you find on the grid element) is reachable from the DevTools console but is **not covered by semver**. It can change or disappear in any release. Use it for debugging, logging, and one-off scripts — not for production code paths. ::: Common things to check here: - `config.plugins?.map(p => p.constructor.name)` — is the plugin you expect actually attached? Plugins are silently *not* added if their feature import was missed. - `grid.columns` after assignment — did your inferred columns end up where you expected? - `grid.rows.length` vs `grid.sourceRows.length` — does filtering/grouping account for the difference? - `grid._pluginManager` *(internal)* — exposes the live plugin manager (lookup, ordering, hook subscriptions). - `grid._virtualization` *(internal)* — current virtual window: `{ start, end, … }` — useful for "why isn't row N in the DOM?". ### 3. Find your grid (and a specific row/cell) in the DOM The grid uses **Light DOM** — there's no shadow root to break inspection. Standard DevTools "Inspect element" works directly. The grid publishes its class names, data-attribute names, and prebuilt selectors as public constants so your queries don't break if internals change: ```ts import { GridSelectors, GridClasses, GridDataAttrs, queryGrid } from '@toolbox-web/grid'; // Multi-version-safe grid lookup const grid = queryGrid('#employees'); // Or any grid on the page (works across versions thanks to the data attribute): queryGrid('[data-tbw-grid]'); // Use the published selectors — these are stable across versions grid.querySelector(GridSelectors.HEADER_ROW); // header row grid.querySelectorAll(GridSelectors.DATA_ROW); // all rendered data rows grid.querySelectorAll(GridSelectors.DATA_CELL); // all rendered cells grid.querySelectorAll(GridSelectors.SELECTED_ROWS); // selected rows grid.querySelector(GridSelectors.EDITING_CELL); // the currently-editing cell // Find a specific cell by field, or by row index (data-row is on the cell) grid.querySelector(GridSelectors.CELL_BY_FIELD('email')); // first cell in the 'email' column grid.querySelector(`.${GridClasses.DATA_CELL}[data-row="42"][${GridDataAttrs.FIELD}="email"]`); ``` **Internal shortcuts** that are convenient in the DevTools console (subject to the stability note above): ```ts grid.findHeaderRow(); // the header row element grid.findRenderedRowElement(42); // null if row 42 is outside the virtualized window grid._hostElement; // the grid root element grid._renderRoot; // the inner render container ``` > **Virtualization gotcha:** at any moment the DOM only contains rows in the visible viewport + a small overscan buffer. If `grid.querySelectorAll(GridSelectors.DATA_ROW)` doesn't include the row you expect, it exists in `grid.rows` but hasn't been rendered yet — call `grid.scrollToRow(rowIndex)` (or `grid.scrollToRowById(rowId)`) to bring it into view, or operate on the data, not the DOM. ### 4. Watch what the grid is doing — listen to lifecycle events Most "is the grid even re-rendering?" / "did my sort actually apply?" questions are answered in five seconds by attaching a listener. Every event is keyed in the typed `DataGridEventMap` — pass the name as a plain string literal and the detail is type-checked for you: ```ts import type { DataGridEventMap } from '@toolbox-web/grid'; // Log every render grid.on('render', (detail) => console.log('render', detail)); // Log every sort / filter / column / selection change grid.on('sort-change', (detail) => console.log('sort', detail)); grid.on('filter-change', (detail) => console.log('filter', detail)); grid.on('column-state-change', (detail) => console.log('columns', detail)); grid.on('selection-change', (detail) => console.log('selection', detail)); // Log a representative set at once. This is a curated subset, not the full // catalog — the `keyof DataGridEventMap` type is the complete, type-checked // list of event names; add any others you need to the array below. const allEvents: (keyof DataGridEventMap)[] = [ 'render', 'sort-change', 'filter-change', 'column-state-change', 'selection-change', ]; for (const name of allEvents) { grid.on(name, (detail) => console.log(name, detail)); } ``` `grid.on()` returns a disposer (`() => void`) — call it to stop listening. Useful patterns: - **"My edit isn't saving"** → listen for `'cell-commit'` / `'row-commit'` and `'edit-close'`; see which one fires. - **"My data appears stale"** → log `'render'` after every data mutation; if it doesn't fire, the grid isn't seeing the change (most often: you mutated `rows` in place instead of assigning a new array). - **"Selection state is wrong"** → log `'selection-change'` to see what the grid thinks is selected. ### 5. Diagnose a hot path with the Performance tab Anything that *works but is slow* is a performance question, not a config question. The [Performance guide](https://toolboxjs.com/grid/guides/performance.md#profile-your-own-app) has a flame-graph reading walkthrough that distinguishes grid-internal work from framework reconciliation from layout thrashing. Start there. ### 6. Diagnose a plugin Plugin issues usually trace to one of three causes: 1. **Plugin not attached.** Confirm via `(await grid.getConfig()).plugins`. If your column uses `editable: true` and you see no `EditingPlugin` in the list, you forgot the feature import. 2. **Hook order conflict.** Two plugins manipulating the same columns/rows — see [Two plugins modifying the same columns/rows](#two-plugins-modifying-the-same-columnsrows). 3. **Hook too slow.** Plugin work in `afterCellRender` / `afterRowRender` shows up in the flame graph during scroll. See [Hook performance budget](https://toolboxjs.com/grid/plugin-development/architecture.md#hook-performance-budget). When filing a plugin bug, include the output of: ```ts const config = await grid.getConfig(); console.log(config.plugins?.map(p => ({ name: p.constructor.name, config: p, }))); ``` ### Quick reference: debug APIs Stable APIs (covered by semver): | API | Returns | Use when | |---|---|---| | `queryGrid(selector)` | grid element (or `null`) | Multi-version-safe lookup | | `grid.ready()` | `Promise<void>` | Wait for first render before assertions | | `grid.getConfig()` | `Promise<Readonly<GridConfig>>` | "What did the grid actually merge?" — async, frozen | | `grid.columns` / `grid.rows` | current arrays | After-the-fact inspection | | `grid.sourceRows` | unfiltered/unsorted data | Distinguish view from source | | `grid.getRow(id)` / `grid.getRowId(row)` | row / string | Row identity debugging | | `grid.on(name, listener)` | disposer | Watch what the grid is doing | | `DGEvents` / `PluginEvents` | event-name catalog | "What can I even listen for?" | | `GridSelectors` / `GridClasses` / `GridDataAttrs` | stable selectors and attribute names | Standard DOM queries against the grid | | `grid.scrollToRow(index)` / `grid.scrollToRowById(id)` | `void` | Bring a virtualized row into view | | `grid.forceLayout()` | `Promise<void>` | Re-measure after content change | Internal helpers (reachable from the DevTools console — not covered by semver): | API | Returns | Use when | |---|---|---| | `grid.effectiveConfig` | `GridConfig` (sync, live) | Quick sync peek at merged config from the console | | `grid.findHeaderRow()` | `HTMLElement` | Direct header lookup | | `grid.findRenderedRowElement(index)` | element or `null` | DOM lookup respecting virtualization | | `grid._pluginManager` | `PluginManager` | Inspect plugin ordering, hook subscriptions | | `grid._virtualization` | `{ start, end, … }` | See the current virtual window | | `grid._hostElement` / `grid._renderRoot` | `HTMLElement` | Direct internal DOM access | | `grid.disconnectSignal` | `AbortSignal` | Auto-cleanup for ad-hoc listeners | --- ## Height & Virtualization The grid virtualizes rows for performance, which requires a **constrained height**. This is the most common setup issue. ### Grid appears empty or collapsed The grid needs an explicit height. Without one, the container collapses to zero and nothing renders. ```css /* ✅ Fixed height */ tbw-grid { height: 500px; } /* ✅ Flex layout */ .container { display: flex; flex-direction: column; height: 100vh; } tbw-grid { flex: 1; min-height: 0; /* Required for flex children to shrink */ } ``` If you don't need virtual scrolling (small datasets), set `height: auto` so the grid sizes to its content: ```css tbw-grid { height: auto; } ``` ### Rows appear in wrong positions after scroll Variable row heights can cause position drift. Use a fixed `rowHeight` for best accuracy: ```typescript grid.gridConfig = { rowHeight: 36 }; ``` If you need auto-measured heights, call `forceLayout()` after content changes: ```typescript await grid.forceLayout(); ``` ### Blank rows or flickering during fast scroll The grid renders a buffer of extra rows (overscan) above and below the viewport. During very fast scrolling, the buffer can be exhausted before new rows paint. Setting `rowHeight` explicitly lets the grid calculate positions without measuring, which keeps up better: ```typescript grid.gridConfig = { rowHeight: 32 }; ``` --- ## Performance ### Use formatters over renderers for simple values `format` returns a string — significantly faster than creating DOM elements in a `renderer`: ```typescript // ✅ Fast — string formatter { field: 'salary', format: (v) => `$${v.toLocaleString()}` } // ⚠️ Slower — DOM renderer (use only when you need rich content) { field: 'salary', renderer: (ctx) => { const el = document.createElement('span'); el.textContent = `$${ctx.value.toLocaleString()}`; return el; }} ``` ### Column virtualization for wide grids For 50+ columns, enable the `columnVirtualization` feature so off-screen columns aren't rendered: ```typescript import '@toolbox-web/grid/features/column-virtualization'; grid.gridConfig = { features: { columnVirtualization: true }, }; ``` ### Disable animations during initial load If first paint feels slow, disable animations: ```typescript grid.gridConfig = { animation: { mode: 'off' } }; ``` See the [Performance Guide](https://toolboxjs.com/grid/guides/performance.md) for profiling techniques and detailed tuning. --- ## For Plugin & Adapter Developers The sections below cover issues specific to **custom plugin** and **framework adapter** development. ### Plugin dependency errors A plugin requires another plugin that isn't loaded: ``` UndoRedoPlugin requires EditingPlugin ``` **Fix with features API** (auto-resolves dependencies): ```typescript import '@toolbox-web/grid/features/undo-redo'; // EditingPlugin is loaded automatically ``` **Fix with plugins API** (manual ordering): ```typescript plugins: [ new EditingPlugin(), // Must come first new UndoRedoPlugin(), // Depends on EditingPlugin ] ``` ### Two plugins modifying the same columns/rows **Symptoms**: Unexpected column order, missing columns, or data appearing twice. Plugins process columns and rows in registration order. If two plugins manipulate the same columns, they may conflict. When using the features API, try reordering the `features` entries. When using the plugins API, reorder the array — the last plugin to process wins. ### Known plugin incompatibilities | Plugin A | Plugin B | Issue | |----------|----------|-------| | GroupingRowsPlugin | TreePlugin | Both transform the entire row model | | GroupingRowsPlugin | PivotPlugin | Pivot creates its own row/column structure | | TreePlugin | PivotPlugin | Pivot replaces the data structure | | ServerSidePlugin | GroupingRows/Tree/Pivot | These require the full dataset; server-side loads lazily | See the [Plugins Overview → Known Incompatibilities](https://toolboxjs.com/grid/plugins.md#known-incompatibilities) for the full list. ### Styles not applying (CSP) The grid injects plugin styles via `adoptedStyleSheets`. If your CSP blocks this, styles fail silently. **Required CSP header:** ``` style-src 'self'; ``` `adoptedStyleSheets` does not require `'unsafe-inline'` — it's treated as a programmatic stylesheet. Check the browser console for CSP violation messages. ### CSS cascade layer specificity The grid's built-in styles use CSS `@layer`. Your custom CSS may not be specific enough to override them. **Use CSS custom properties** (recommended): ```css tbw-grid { --tbw-color-row-hover: #e0f7fa; --tbw-cell-padding: 12px 16px; } ``` Since the grid uses Light DOM, normal CSS selectors also work — just ensure your selectors are specific enough to beat the `@layer` defaults. --- ## Still Stuck? If you've worked through [How to debug the grid](#how-to-debug-the-grid) and the issue isn't covered above, the fastest path to help is a minimal reproduction: 1. **Strip your code to the smallest example that still reproduces the issue** — one grid, the minimum config, ideally in a [StackBlitz](https://stackblitz.com/) or a single HTML file with the UMD bundle. 2. **Capture the diagnostic output** — copy the TBW error code and full prefixed message from the console. 3. **Capture the effective config** — `console.log(await grid.getConfig())` and include the plugin list. 4. **Search [existing GitHub issues](https://github.com/OysteinAmundsen/toolbox/issues)** — someone may have hit the same wall. 5. **Open a new issue** with reproduction, browser/OS, grid version, and the captures above. --- # Clipboard Plugin > Copy and paste grid data with Excel-compatible clipboard support. The Clipboard plugin brings familiar copy/paste functionality to your grid with full keyboard shortcut support (Ctrl+C, Ctrl+V). It handles single cells, multi-cell selections, and interoperates with Excel and other spreadsheet applications via tab-delimited output. :::tip Works best with [SelectionPlugin](https://toolboxjs.com/grid/plugins/selection.md) for copying/pasting selected cells. Without it, copies the entire grid and pastes at row 0, column 0. ::: ## Installation ```ts import '@toolbox-web/grid/features/clipboard'; ``` ## Basic Usage Just enable the feature and you're ready to go—keyboard shortcuts work automatically. Configure options like `includeHeaders` for copying column headers or `delimiter` for custom CSV formats. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' } ], features: { clipboard: { includeHeaders: true, quoteStrings: true, // Wrap text in quotes for CSV compatibility }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/selection'; import '@toolbox-web/grid-react/features/clipboard'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' } ], features: { selection: 'range', clipboard: { includeHeaders: true, quoteStrings: true }, }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}`, `selection="range"`, and `clipboard={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/selection'; import '@toolbox-web/grid-vue/features/clipboard'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Alice', email: 'alice@example.com', department: 'Engineering' }, { name: 'Bob', email: 'bob@example.com', department: 'Marketing' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { selection: 'range', clipboard: { includeHeaders: true, quoteStrings: true }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead set `selection="range"`, bind `:clipboard="{ … }"`, and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/selection'; import '@toolbox-web/grid-angular/features/clipboard'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' } ], features: { selection: 'range', clipboard: { includeHeaders: true, quoteStrings: true }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridSelectionDirective` and `GridClipboardDirective` and bind `[selection]` and `[clipboard]` directly on `<tbw-grid>`. That form gives a compile-time check that the features are imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Clipboard Plugin](https://toolboxjs.com/grid/plugins/clipboard.md) --- # Column Virtualization Plugin > Improve performance for grids with many columns by only rendering visible columns. The Column Virtualization plugin improves performance for grids with many columns. ## Installation ```ts import '@toolbox-web/grid/features/column-virtualization'; ``` ## Basic Usage #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/column-virtualization'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: generateManyColumns(100), // Your column generator fitMode: 'fixed', // Required for column virtualization features: { columnVirtualization: true }, }; grid.rows = data; ``` #### React ```tsx import '@toolbox-web/grid-react/features/column-virtualization'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: generateManyColumns(100), fitMode: 'fixed', features: { columnVirtualization: true }, }; function WideGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `columnVirtualization={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/column-virtualization'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [{ col0: 'Row 1', col1: 'Value 1' }, { col0: 'Row 2', col1: 'Value 2' }]; const gridConfig: GridConfig = { columns: Array.from({ length: 100 }, (_, i) => ({ field: `col${i}`, header: `Column ${i + 1}`, width: 120, })), fitMode: 'fixed', features: { columnVirtualization: true }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:column-virtualization="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/column-virtualization'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { ColumnConfig, GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-wide-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class WideGridComponent { rows = [...]; gridConfig: GridConfig = { columns: this.generateManyColumns(100), fitMode: 'fixed', features: { columnVirtualization: true }, }; generateManyColumns(count: number): ColumnConfig[] { return Array.from({ length: count }, (_, i) => ({ field: `col${i}`, header: `Column ${i + 1}`, width: 120, })); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridColumnVirtualizationDirective` and bind `[columnVirtualization]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Column Virtualization Plugin](https://toolboxjs.com/grid/plugins/column-virtualization.md) --- # Context Menu Plugin > Add right-click context menus to the grid with customizable items. The Context Menu plugin adds a customizable right-click menu to your grid cells. Build anything from simple copy/paste actions to complex nested menus with conditional visibility, icons, and keyboard shortcuts. ## Installation ```ts import '@toolbox-web/grid/features/context-menu'; ``` ## Basic Usage Define your menu items as an array—each item has an `id`, `label`, and `action` callback that receives context about the clicked cell (row data, column info, cell value, etc.). Add separators between groups of actions for visual clarity. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); function removeRow(rowIndex: number) { grid.rows = grid.rows.filter((_, i) => i !== rowIndex); } grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' } ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx) => removeRow(ctx.rowIndex) }, ], }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/context-menu'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; function MyGrid({ data, onDelete }) { const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' } ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx) => onDelete(ctx.rowIndex) }, ], }, }, }; return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `contextMenu={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/context-menu'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Alice', email: 'alice@example.com', status: 'active' }, { name: 'Bob', email: 'bob@example.com', status: 'inactive' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' }, ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx) => console.log('Delete row:', ctx.rowIndex) }, ], }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:context-menu="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/context-menu'; import { Component, output } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; import type { ContextMenuItem } from '@toolbox-web/grid/plugins/context-menu'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { deleteRow = output<number>(); rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' } ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx: any) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx: any) => this.deleteRow.emit(ctx.rowIndex) }, ] as ContextMenuItem[], }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridContextMenuDirective` and bind `[contextMenu]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Context Menu Plugin](https://toolboxjs.com/grid/plugins/context-menu.md) --- # Editing Plugin > Enable inline cell editing with built-in and custom editors. The Editing plugin enables inline cell editing in the grid. It provides built-in editors for common data types and supports custom editor functions for specialized input scenarios. ## Installation ```ts import '@toolbox-web/grid/features/editing'; ``` ## Basic Usage Enable the editing feature to use `editable` and `editor` column properties: #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/editing'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'active', header: 'Active', type: 'boolean', editable: true }, ], features: { editing: 'dblclick' }, // or 'click' }; // Listen for commits grid.on('cell-commit', (detail) => { console.log('Cell edited:', detail); }); ``` #### React ```tsx import '@toolbox-web/grid-react/features/editing'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'active', header: 'Active', type: 'boolean', editable: true }, ], features: { editing: 'dblclick' }, }; function MyGrid({ data }) { return ( <DataGrid rows={data} gridConfig={gridConfig} onCellCommit={(e) => console.log('Edited:', e.detail)} style={{ height: '400px' }} /> ); } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `editing="dblclick"` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/editing'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { id: 1, name: 'Widget', price: 9.99, active: true }, { id: 2, name: 'Gadget', price: 19.99, active: false }, ]; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'active', header: 'Active', type: 'boolean', editable: true }, ], features: { editing: 'dblclick' }, }; const onCellCommit = (e) => { console.log('Cell edited:', e.detail); }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" @cell-commit="onCellCommit" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:editing="'dblclick'"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/editing'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid], template: ` <tbw-grid [rows]="data" [gridConfig]="gridConfig" (cellCommit)="onCommit($event)" /> ` }) export class MyGridComponent { data = [...]; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, ], features: { editing: true }, }; onCommit(event: CustomEvent) { console.log('Edited:', event.detail); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridEditingDirective` and bind `[editing]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: ### Try basic editing ```ts // EditingBasicEditingDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/editing'; const container = document.getElementById('editing-basic-editing-demo'); if (container) { const grid = queryGrid('tbw-grid', container); grid.gridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'score', header: 'Score', type: 'number', editable: true }, { field: 'role', header: 'Role', type: 'select', editable: true, options: [ { label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }, { label: 'Guest', value: 'guest' }, ], }, ], features: { editing: 'dblclick' }, }; grid.rows = [ { name: 'Alice', score: 95, role: 'admin' }, { name: 'Bob', score: 82, role: 'user' }, { name: 'Carol', score: 91, role: 'guest' }, ]; grid.on('cell-commit', ({ field, newValue }) => { console.log('Edited:', field, '→', newValue); }); } ``` Double-click any cell to start editing. Press Enter to commit or Escape to cancel. **Full plugin API — configuration options, events, methods, and advanced usage:** [Editing Plugin](https://toolboxjs.com/grid/plugins/editing.md) --- # Export Plugin > Export grid data to CSV or other formats. The Export plugin lets users download grid data as CSV, JSON, or other formats with a single click or API call. Great for reporting, data backup, or letting users work with data in Excel. ## Installation ```ts import '@toolbox-web/grid/features/export'; ``` ## Basic Usage Enable the feature and call `exportCsv()` or `exportJson()` when you're ready to download. You can also combine it with the selection feature to export only selected rows. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' } ], features: { export: { fileName: 'employees', includeHeaders: true, }, }, }; // Trigger export via button click document.getElementById('export-btn').addEventListener('click', () => { const plugin = grid.getPluginByName('export'); plugin.exportCsv(); }); ``` #### React ```tsx import '@toolbox-web/grid-react/features/export'; import { DataGrid, useGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { export: { fileName: 'employees', includeHeaders: true }, }, }; function EmployeeGrid({ data }) { const { ref, element } = useGrid(); const handleExport = () => { element?.getPluginByName('export')?.exportCsv(); }; return ( <> <button onClick={handleExport}>Export CSV</button> <DataGrid ref={ref} rows={data} gridConfig={gridConfig} style={{ height: '400px' }} /> </> ); } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `export={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/export'; import { TbwGrid, useGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Alice', email: 'alice@example.com', department: 'Engineering' }, { name: 'Bob', email: 'bob@example.com', department: 'Marketing' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { export: { fileName: 'employees', includeHeaders: true }, }, }; const { gridElement } = useGrid(); const handleExport = () => { gridElement.value?.getPluginByName('export')?.exportCsv(); }; </script> <template> <div> <button @click="handleExport">Export CSV</button> <TbwGrid :rows="data" :grid-config="gridConfig" /> </div> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:export="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/export'; import { Component } from '@angular/core'; import { Grid, injectGrid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-data-grid', imports: [Grid], template: ` <button (click)="handleExport()">Export CSV</button> <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class DataGridComponent { grid = injectGrid(); rows = []; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { export: { fileName: 'employees', includeHeaders: true }, }, }; handleExport() { const plugin = this.grid.element()?.getPluginByName('export'); plugin?.exportCsv(); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridExportDirective` and bind `[exportFeature]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Export Plugin](https://toolboxjs.com/grid/plugins/export.md) --- # Filtering Plugin > Add column-level filtering with built-in filter panel and custom filters. The Filtering plugin adds column header filters with text search, dropdown options, and custom filter panels. It supports both local filtering for small datasets and async handlers for server-side filtering on large datasets. ## Installation ```ts import '@toolbox-web/grid/features/filtering'; ``` ## Basic Usage #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/filtering'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ], features: { filtering: { debounceMs: 300 } }, }; grid.rows = data; ``` #### React ```tsx import '@toolbox-web/grid-react/features/filtering'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ], features: { filtering: { debounceMs: 300 } }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `filtering={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/filtering'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Alice', status: 'active', email: 'alice@example.com' }, { name: 'Bob', status: 'inactive', email: 'bob@example.com' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ], features: { filtering: { debounceMs: 300 } }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:filtering="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/filtering'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ], features: { filtering: { debounceMs: 300 } }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridFilteringDirective` and bind `[filtering]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Filtering Plugin](https://toolboxjs.com/grid/plugins/filtering.md) --- # Column Grouping Plugin > Group columns visually under shared parent headers. The Column Grouping plugin enables visual grouping of columns under shared headers. ## Installation ```ts import '@toolbox-web/grid/features/grouping-columns'; ``` ## Basic Usage There are three ways to define column groups, from most to least recommended: ### Option 1: Feature config `columnGroups` (Recommended) Define everything in one place — groups, renderers, and plugin behavior: #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/grouping-columns'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/grouping-columns'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }, }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `groupingColumns={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/grouping-columns'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [...]; const gridConfig: GridConfig = { columns: [ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:grouping-columns="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/grouping-columns'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'firstName', header: 'First Name' }, { field: 'lastName', header: 'Last Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'title', header: 'Title' }, { field: 'salary', header: 'Salary' }, ], features: { groupingColumns: { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridGroupingColumnsDirective` and bind `[groupingColumns]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: :::tip[Auto-generated IDs] The `id` property on each group is optional. When omitted, it is auto-generated as a slug of the `header` (e.g. `'Personal Info'` → `'personal-info'`). Provide an explicit `id` when you need a stable identifier for programmatic access or custom renderers. ::: ### Option 2: Grid config `columnGroups` If your groups are part of a server-provided layout or shared data model, define them on `gridConfig.columnGroups`: ```ts grid.gridConfig = { columnGroups: [ { header: 'Personal Info', children: ['firstName', 'lastName', 'email'] }, { header: 'Work Info', children: ['department', 'title', 'salary'] }, ], columns: [...], features: { groupingColumns: true }, }; ``` :::note[Precedence] If `columnGroups` is defined in **both** the feature config and `gridConfig`, the feature config wins and a console warning is emitted. ::: ### Option 3: Inline `group` property For simple cases, assign group membership directly on each column. The `group` string becomes the group id: ```ts grid.gridConfig = { columns: [ { field: 'firstName', header: 'First Name', group: 'personal' }, { field: 'lastName', header: 'Last Name', group: 'personal' }, { field: 'department', header: 'Department', group: 'work' }, ], features: { groupingColumns: true }, }; ``` You can also pass an object with an explicit label: ```ts { field: 'firstName', header: 'First Name', group: { id: 'personal', label: 'Personal Info' } } ``` :::caution The inline `group` property is strictly for declaring group membership. It does not support custom renderers — use `columnGroups` with `renderer` or `groupHeaderRenderer` for that. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Column Grouping Plugin](https://toolboxjs.com/grid/plugins/grouping-columns.md) --- # Row Grouping Plugin > Group rows by column values with expandable groups. The Row Grouping plugin organizes your rows into collapsible hierarchical groups. Perfect for organizing data by category, department, status, or any other dimension—or even multiple dimensions for nested grouping. ## Installation ```ts import '@toolbox-web/grid/features/grouping-rows'; ``` ## Basic Usage The `groupOn` callback receives each row and should return an array representing the group path. For single-level grouping, return a one-element array. For multi-level grouping, return multiple elements (e.g., `['Region', 'Department']`). #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' } ], features: { groupingRows: { groupOn: (row) => [row.department], showRowCount: true, defaultExpanded: false, }, }, }; grid.rows = employees; ``` #### React ```tsx import '@toolbox-web/grid-react/features/grouping-rows'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' } ], features: { groupingRows: { groupOn: (row) => row.department, showRowCount: true }, }, }; function EmployeeGrid({ employees }) { return <DataGrid rows={employees} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `groupingRows={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/grouping-rows'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const employees = [ { name: 'Alice', department: 'Engineering', salary: 95000 }, { name: 'Bob', department: 'Marketing', salary: 75000 }, { name: 'Carol', department: 'Engineering', salary: 90000 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { groupingRows: { groupOn: (row) => row.department, showRowCount: true }, }, }; </script> <template> <TbwGrid :rows="employees" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:grouping-rows="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/grouping-rows'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <tbw-grid [rows]="employees" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class EmployeeGridComponent { employees = [/* employee data */]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' } ], features: { groupingRows: { groupOn: (row: any) => [row.department], showRowCount: true, }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridGroupingRowsDirective` and bind `[groupingRows]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Row Grouping Plugin](https://toolboxjs.com/grid/plugins/grouping-rows.md) --- # Master-Detail Plugin > Show expandable detail rows beneath data rows. The Master-Detail plugin lets you create expandable detail rows that reveal additional content beneath each master row. Perfect for order/line-item UIs, employee/department views, or any scenario where you need to show related data without navigating away. ## Installation ```ts import '@toolbox-web/grid/features/master-detail'; ``` ## Basic Usage The key configuration is `detailRenderer` - a function that receives the row data and returns either an HTML string or a DOM element. This gives you complete control over what appears in the expanded detail area. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'orderId', header: 'Order ID' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'currency' } ], features: { masterDetail: { detailRenderer: (row) => ` <div class="order-details"> <h4>Order Items</h4> <ul> ${row.items.map(item => `<li>${item.name} - $${item.price}</li>`).join('')} </ul> </div> `, }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/master-detail'; import { DataGrid, GridDetailPanel } from '@toolbox-web/grid-react'; function OrderGrid({ orders }) { return ( <DataGrid rows={orders} columns={[ { field: 'orderId', header: 'Order ID' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'currency' } ]} style={{ height: '400px' }} > <GridDetailPanel> {({ row }) => ( <div className="order-details"> <h4>Order Items</h4> <ul> {row.items.map(item => ( <li key={item.id}>{item.name} - ${item.price}</li> ))} </ul> </div> )} </GridDetailPanel> </DataGrid> ); } ``` #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/master-detail'; import { TbwGrid, TbwGridColumn, TbwGridDetailPanel } from '@toolbox-web/grid-vue'; const orders = [ { orderId: 'ORD-001', customer: 'Alice', total: 150, items: [{ id: 1, name: 'Widget', price: 50 }, { id: 2, name: 'Gadget', price: 100 }] }, { orderId: 'ORD-002', customer: 'Bob', total: 75, items: [{ id: 3, name: 'Tool', price: 75 }] }, ]; </script> <template> <TbwGrid :rows="orders" master-detail> <TbwGridColumn field="orderId" header="Order ID" /> <TbwGridColumn field="customer" header="Customer" /> <TbwGridColumn field="total" header="Total" type="currency" /> <TbwGridDetailPanel v-slot="{ row }"> <div class="order-details"> <h4>Order Items</h4> <ul> <li v-for="item in row.items" :key="item.id"> {{ item.name }} - ${{ item.price }} </li> </ul> </div> </TbwGridDetailPanel> </TbwGrid> </template> ``` #### Angular ```typescript // Feature imports - GridMasterDetailDirective owns the [masterDetail] input, // GridDetailView is the structural directive used inside <tbw-grid> for templating. import { GridMasterDetailDirective, GridDetailView, } from '@toolbox-web/grid-angular/features/master-detail'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { ColumnConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-order-grid', imports: [Grid, GridMasterDetailDirective, GridDetailView], template: ` <tbw-grid [rows]="orders" [columns]="columns" [masterDetail]="true" style="height: 400px; display: block;"> <ng-template tbwDetailView let-row let-toggle="toggle"> <div class="order-details"> <h4>Order Items</h4> <ul> <li *ngFor="let item of row.items"> {{ item.name }} - \${{ item.price }} </li> </ul> <button (click)="toggle()">Close</button> </div> </ng-template> </tbw-grid> `, }) export class OrderGridComponent { orders = [/* order data */]; columns: ColumnConfig[] = [ { field: 'orderId', header: 'Order ID' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'currency' } ]; } ``` Use the [`GridDetailView`](https://toolboxjs.com/grid/angular/api/directives/griddetailview.md) directive (`<ng-template tbwDetailView>`) for Angular-idiomatic detail panels with template context (`let-row`, `let-toggle="toggle"`). For non-Angular consumers of the same plugin, an inline `detailRenderer: (row) => HTMLElement | string` is also accepted in `gridConfig.masterDetail`. **Full plugin API — configuration options, events, methods, and advanced usage:** [Master-Detail Plugin](https://toolboxjs.com/grid/plugins/master-detail.md) --- # Multi-Sort Plugin > Sort by multiple columns with shift-click support. This plugin is not needed if you only want single-column sorting — set `sortable: true` on the columns. This is built in to the core. Multi-Sort extends that functionality by enabling sorting by multiple columns at once—hold Shift and click additional column headers to build up a sort stack. Priority badges show the sort order, so users always know which column takes precedence. ## Installation ```ts import '@toolbox-web/grid/features/multi-sort'; ``` ## Basic Usage Mark columns as `sortable: true` and enable the feature. Users Shift+click to add columns to the sort stack, and regular click to reset to single-column sort. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, { field: 'startDate', header: 'Start Date', type: 'date', sortable: true }, ], features: { multiSort: { maxSortColumns: 3, showSortIndex: true, }, }, }; // Listen for sort changes grid.on('sort-change', ({ sortModel }) => { console.log('Active sorts:', sortModel); }); ``` #### React ```tsx import '@toolbox-web/grid-react/features/multi-sort'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, ], features: { multiSort: { maxSortColumns: 3 } }, }; function SortableGrid({ data }) { return ( <DataGrid rows={data} gridConfig={gridConfig} onSortChange={(detail) => console.log('Sort changed:', detail.sortModel)} style={{ height: '400px' }} /> ); } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `multiSort={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/multi-sort'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Alice', department: 'Engineering', salary: 95000 }, { name: 'Bob', department: 'Marketing', salary: 75000 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, ], features: { multiSort: { maxSortColumns: 3 } }, }; const onSortChange = (e) => { console.log('Sort changed:', e.detail.sortModel); }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" @sort-change="onSortChange" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:multi-sort="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/multi-sort'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-sortable-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" (sort-change)="onSortChange($event)" style="height: 400px; display: block;"> </tbw-grid> `, }) export class SortableGridComponent { rows = [ { name: 'Alice', department: 'Engineering', salary: 95000 }, { name: 'Bob', department: 'Marketing', salary: 75000 }, ]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, ], features: { multiSort: { maxSortColumns: 3 } }, }; onSortChange(e: CustomEvent) { console.log('Sort changed:', e.detail.sortModel); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridMultiSortDirective` and bind `[multiSort]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Multi-Sort Plugin](https://toolboxjs.com/grid/plugins/multi-sort.md) --- # Pinned Columns Plugin > Pin columns to the left or right side of the grid. The Pinned Columns plugin freezes columns to the left or right edge of the grid—essential for keeping key identifiers or action buttons visible while scrolling through wide datasets. Just set `pinned: 'left'` or `pinned: 'right'` on your column definitions and the plugin handles the rest. ## Installation ```ts import '@toolbox-web/grid/features/pinned-columns'; ``` ## Basic Usage Pin important columns like ID on the left and action buttons on the right. The pinned columns stay fixed while the middle content scrolls horizontally. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID', pinned: 'left', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, { field: 'actions', header: 'Actions', pinned: 'right', width: 120 }, ], features: { pinnedColumns: true }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/pinned-columns'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID', pinned: 'left', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, { field: 'actions', header: 'Actions', pinned: 'right', width: 120 }, ], features: { pinnedColumns: true }, }; function EmployeeGrid({ employees }) { return <DataGrid rows={employees} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `pinnedColumns` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/pinned-columns'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const employees = [ { id: 1, name: 'Alice', email: 'alice@example.com', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', email: 'bob@example.com', department: 'Marketing', salary: 75000 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID', pinned: 'left', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, { field: 'actions', header: 'Actions', pinned: 'right', width: 120 }, ], features: { pinnedColumns: true }, }; </script> <template> <TbwGrid :rows="employees" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:pinned-columns="true"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/pinned-columns'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class EmployeeGridComponent { rows = []; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID', pinned: 'left', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, { field: 'actions', header: 'Actions', pinned: 'right', width: 120 }, ], features: { pinnedColumns: true }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridPinnedColumnsDirective` and bind `[pinnedColumns]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Pinned Columns Plugin](https://toolboxjs.com/grid/plugins/pinned-columns.md) --- # Pinned Rows (Status Bar) Plugin > Pin summary or custom rows to the top or bottom of the grid. The Pinned Rows plugin creates a fixed status bar at the top or bottom of the grid for displaying aggregations, row counts, or custom content. Think of it as the "totals row" you'd see in a spreadsheet—always visible regardless of scroll position. ## Installation ```ts import '@toolbox-web/grid/features/pinned-rows'; ``` ## Basic Usage Pinned rows are configured with a unified `slots[]` array. Each slot becomes its own DOM row at `position: 'top'` or `position: 'bottom'` (default `'bottom'`), in declared order. A slot is either an **aggregation row** (sum/avg/min/max/count/first/last or a custom function per column) or a **panel row** (a `render` function — built-in or custom). #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import { rowCountPanel } from '@toolbox-web/grid/plugins/pinned-rows'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'product', header: 'Product' }, { field: 'quantity', header: 'Qty', type: 'number' }, { field: 'price', header: 'Price', type: 'currency' }, ], features: { pinnedRows: { slots: [ { id: 'totals', position: 'bottom', aggregators: { quantity: 'sum', price: { aggFunc: 'sum', formatter: (v) => `$${v.toFixed(2)}` }, }, cells: { product: 'Totals:' }, }, { id: 'count', position: 'bottom', render: rowCountPanel() }, ], }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/pinned-rows'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; import { rowCountPanel } from '@toolbox-web/grid/plugins/pinned-rows'; const gridConfig: GridConfig = { columns: [ { field: 'product', header: 'Product' }, { field: 'quantity', header: 'Qty', type: 'number' }, { field: 'price', header: 'Price', type: 'currency' }, ], features: { pinnedRows: { slots: [ { id: 'totals', position: 'bottom', aggregators: { quantity: 'sum', price: 'sum' }, cells: { product: 'Totals:' }, }, { id: 'count', position: 'bottom', render: rowCountPanel() }, ], }, }, }; function OrderGrid({ orders }) { return <DataGrid rows={orders} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `pinnedRows={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/pinned-rows'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; import { rowCountPanel } from '@toolbox-web/grid/plugins/pinned-rows'; const orders = [ { product: 'Widget', quantity: 10, price: 99.99 }, { product: 'Gadget', quantity: 5, price: 149.99 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'product', header: 'Product' }, { field: 'quantity', header: 'Qty', type: 'number' }, { field: 'price', header: 'Price', type: 'currency' }, ], features: { pinnedRows: { slots: [ { id: 'totals', position: 'bottom', aggregators: { quantity: 'sum', price: 'sum' }, cells: { product: 'Totals:' }, }, { id: 'count', position: 'bottom', render: rowCountPanel() }, ], }, }, }; </script> <template> <TbwGrid :rows="orders" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:pinned-rows="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/pinned-rows'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; import { rowCountPanel } from '@toolbox-web/grid/plugins/pinned-rows'; @Component({ selector: 'app-order-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class OrderGridComponent { rows = []; gridConfig: GridConfig = { columns: [ { field: 'product', header: 'Product' }, { field: 'quantity', header: 'Qty', type: 'number' }, { field: 'price', header: 'Price', type: 'currency' }, ], features: { pinnedRows: { slots: [ { id: 'totals', position: 'bottom' as const, aggregators: { quantity: 'sum', price: 'sum' }, cells: { product: 'Totals:' }, }, { id: 'count', position: 'bottom' as const, render: rowCountPanel() }, ], }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridPinnedRowsDirective` and bind `[pinnedRows]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Pinned Rows (Status Bar) Plugin](https://toolboxjs.com/grid/plugins/pinned-rows.md) --- # Pivot Table Plugin > Transform row data into a cross-tabulation (pivot table) layout. The Pivot plugin transforms flat data into a pivot table view. ## Installation ```ts import '@toolbox-web/grid/features/pivot'; ``` ## Basic Usage #### TypeScript ```ts import '@toolbox-web/grid'; import '@toolbox-web/grid/features/pivot'; import { queryGrid } from '@toolbox-web/grid'; const salesData = [ { region: 'North', product: 'Widget', quarter: 'Q1', sales: 1200 }, { region: 'North', product: 'Widget', quarter: 'Q2', sales: 1500 }, { region: 'South', product: 'Gadget', quarter: 'Q1', sales: 900 }, { region: 'South', product: 'Gadget', quarter: 'Q2', sales: 1100 }, ]; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, }, }; grid.rows = salesData; ``` #### React ```tsx import '@toolbox-web/grid-react/features/pivot'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, }, }; function SalesPivot({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `pivot={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/pivot'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { region: 'North', product: 'Widget', quarter: 'Q1', sales: 1000 }, { region: 'North', product: 'Gadget', quarter: 'Q1', sales: 1500 }, { region: 'South', product: 'Widget', quarter: 'Q2', sales: 1200 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:pivot="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/pivot'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-sales-pivot', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class SalesPivotComponent { rows = [...]; // Your sales data gridConfig: GridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridPivotDirective` and bind `[pivot]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Pivot Table Plugin](https://toolboxjs.com/grid/plugins/pivot.md) --- # Print Plugin > Print the grid contents with configurable page settings. The **Print Plugin** enables printing the full grid content by temporarily disabling virtualization and applying print-optimized styles. It handles large datasets gracefully with configurable row limits. ## Installation ```typescript import '@toolbox-web/grid/features/print'; ``` ## Basic Usage Use the controls to try different orientations, toggle title/timestamp, isolated printing, and the toolbar button. Enable "Toolbar button" to add a print icon to the grid header (requires `shell.header` config). ```ts // PrintBasicDemo.astro import '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/print'; const container = document.getElementById('print-basic-demo'); if (container) { const grid = queryGrid('tbw-grid', container)!; const btn = container.querySelector('#print-basic-btn'); const employeeData = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 85000, status: 'Active', email: 'alice@company.com' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 72000, status: 'Active', email: 'bob@company.com' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 92000, status: 'Active', email: 'carol@company.com' }, { id: 4, name: 'David Brown', department: 'Sales', salary: 68000, status: 'On Leave', email: 'david@company.com' }, { id: 5, name: 'Emma Davis', department: 'HR', salary: 65000, status: 'Active', email: 'emma@company.com' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 88000, status: 'Active', email: 'frank@company.com' }, { id: 7, name: 'Grace Wilson', department: 'Marketing', salary: 75000, status: 'Active', email: 'grace@company.com' }, { id: 8, name: 'Henry Taylor', department: 'Sales', salary: 70000, status: 'Active', email: 'henry@company.com' }, { id: 9, name: 'Ivy Anderson', department: 'Finance', salary: 82000, status: 'On Leave', email: 'ivy@company.com' }, { id: 10, name: 'Jack Thomas', department: 'Engineering', salary: 95000, status: 'Active', email: 'jack@company.com' }, ]; let opts: Record<string, unknown> = { orientation: 'landscape', button: false, includeTitle: true, includeTimestamp: true, isolate: true, }; function applyConfig() { grid.gridConfig = { fitMode: 'stretch', ...(opts.button ? { shell: { header: { title: 'Employee Report' } } } : {}), columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', minWidth: 150 }, { field: 'department', header: 'Department', width: 120 }, { field: 'email', header: 'Email', minWidth: 200 }, { field: 'salary', header: 'Salary', width: 100, align: 'right' }, { field: 'status', header: 'Status', width: 100 }, ], features: { print: { orientation: opts.orientation as string, button: opts.button as boolean, title: 'Employee Report', includeTitle: opts.includeTitle as boolean, includeTimestamp: opts.includeTimestamp as boolean, }, }, }; grid.rows = employeeData; } applyConfig(); container.addEventListener('control-change', ((e: CustomEvent) => { opts = e.detail.allValues; applyConfig(); }) as EventListener); btn?.addEventListener('click', () => { const plugin = grid.getPluginByName('print'); plugin?.print({ isolate: opts.isolate as boolean }); }); } ``` #### TypeScript ```typescript import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/print'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary' }, ], features: { print: { title: 'Employee Report', includeTitle: true, includeTimestamp: true, }, }, }; // Programmatic print const plugin = grid.getPluginByName('print'); await plugin.print(); ``` #### React ```tsx import '@toolbox-web/grid-react/features/print'; import { DataGrid } from '@toolbox-web/grid-react'; import { useGridPrint } from '@toolbox-web/grid-react/features/print'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary' }, ], features: { print: { title: 'Employee Report', includeTitle: true, includeTimestamp: true, }, }, }; function EmployeeGrid({ data }) { const { print } = useGridPrint(); const handlePrint = async () => { await print({ title: 'Employee Report' }); }; return ( <div> <button onClick={handlePrint}>Print Report</button> <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} /> </div> ); } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `print={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/print'; import { TbwGrid, useGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Alice', department: 'Engineering', salary: 95000 }, { name: 'Bob', department: 'Marketing', salary: 75000 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary' }, ], features: { print: { title: 'Employee Report', includeTitle: true, includeTimestamp: true }, }, }; const { gridElement } = useGrid(); const handlePrint = async () => { await gridElement.value?.getPluginByName('print')?.print(); }; </script> <template> <div> <button @click="handlePrint">Print Report</button> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </div> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:print="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/print'; import { Component } from '@angular/core'; import { Grid, injectGrid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <button (click)="handlePrint()">Print Report</button> <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class EmployeeGridComponent { grid = injectGrid(); rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary' }, ], features: { print: { title: 'Employee Report', includeTitle: true, includeTimestamp: true } }, }; async handlePrint() { const plugin = this.grid.element()?.getPluginByName('print'); await plugin?.print(); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridPrintDirective` and bind `[print]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Print Plugin](https://toolboxjs.com/grid/plugins/print.md) --- # Column Reorder Plugin > Allow users to reorder columns by drag and drop. The Reorder plugin lets users rearrange columns by dragging and dropping column headers. Supports smooth FLIP animations, fade transitions, or instant reordering depending on your preference. ## Installation ```ts import '@toolbox-web/grid/features/reorder-columns'; ``` ## Basic Usage Just enable the feature and columns become draggable. Users can grab any column header and drop it in a new position. Visual feedback and smooth animations are handled automatically. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { reorderColumns: { animation: 'flip', animationDuration: 200, }, }, }; // Listen for column moves grid.on('column-move', ({ field, fromIndex, toIndex }) => { console.log(`Moved ${field} from ${fromIndex} to ${toIndex}`); }); ``` #### React ```tsx import '@toolbox-web/grid-react/features/reorder-columns'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { reorderColumns: { animation: 'flip' }, }, }; function MyGrid({ data }) { return ( <DataGrid rows={data} gridConfig={gridConfig} onColumnMove={(e) => console.log(`Moved ${e.detail.field} from ${e.detail.fromIndex} to ${e.detail.toIndex}`)} style={{ height: '400px' }} /> ); } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `reorderColumns={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/reorder-columns'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { id: 1, name: 'Alice', email: 'alice@example.com', department: 'Engineering' }, { id: 2, name: 'Bob', email: 'bob@example.com', department: 'Marketing' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { reorderColumns: { animation: 'flip' }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" @column-move="(e) => console.log('Column moved:', e.detail)" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:reorder-columns="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/reorder-columns'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" (column-move)="onColumnMove($event)" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ], features: { reorderColumns: { animation: 'flip' }, }, }; onColumnMove(e: CustomEvent) { console.log(`Moved ${e.detail.field} from ${e.detail.fromIndex} to ${e.detail.toIndex}`); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridReorderColumnsDirective` and bind `[reorderColumns]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Column Reorder Plugin](https://toolboxjs.com/grid/plugins/reorder-columns.md) --- # Responsive Plugin > Automatically adapt the grid layout for different screen sizes. The Responsive plugin transforms the grid from a tabular layout to a card/list layout when the grid width falls below a configurable breakpoint. Use it for narrow containers like split-pane UIs, mobile viewports, and dashboard widgets. ## Installation ```ts import '@toolbox-web/grid/features/responsive'; ``` ## Basic Usage #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/responsive'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, }, }; grid.rows = data; ``` #### React ```tsx import '@toolbox-web/grid-react/features/responsive'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `responsive={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/responsive'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:responsive="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/responsive'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridResponsiveDirective` and bind `[responsive]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Responsive Plugin](https://toolboxjs.com/grid/plugins/responsive.md) --- # Row Drag-Drop Plugin > Drag rows within a single grid (reorder) and across grids that share a drop zone. The Row Drag-Drop plugin lets users rearrange rows by dragging a grip handle or using keyboard shortcuts, and — when opted in via `dropZone` — drag rows between separate grids. It replaces the pre-v3 `RowReorderPlugin`: every intra-grid behaviour that plugin had is preserved. - **Intra-grid** — drag a row up or down to reorder, or use `Ctrl + ↑/↓`. - **Drag origin** — choose whether drags start from the dedicated handle column (`dragFrom: 'handle'`, default), from anywhere on the row (`dragFrom: 'row'` — handle column hidden), or from both (`dragFrom: 'both'`). Interactive descendants (buttons, inputs, links, `[contenteditable]`) inside the row are still respected and never start a drag. - **Cross-grid** — set a `dropZone` to allow dragging rows to another grid with the same `dropZone`. Supports `move` and `copy` operations. - **Multi-row** — when the [Selection](../selection/) plugin is loaded and the dragged row is part of a multi-row selection, all selected rows are dragged together and a count badge appears on the drag image. There is no `selection` config option — the behaviour is automatic. - **Cross-window** — drag rows between separate browser windows or tabs on the same origin. The payload travels via `dataTransfer`, and a same-origin `BroadcastChannel` coordinates the source-side row removal for `operation: 'move'` and the source-side `row-transfer` event. ## Installation ```ts import '@toolbox-web/grid/features/row-drag-drop'; ``` ## Basic Usage Enable the feature and a drag handle column appears automatically. Users can drag rows to new positions or use `Ctrl + ↑/↓` to move the focused row. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/row-drag-drop'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'priority', header: '#', type: 'number', width: 50 }, { field: 'task', header: 'Task' }, { field: 'status', header: 'Status' }, ], features: { rowDragDrop: { dragHandlePosition: 'left', enableKeyboard: true, animation: 'flip', }, }, }; // Listen for row moves (intra-grid) grid.on('row-move', ({ fromIndex, toIndex }) => { console.log(`Moved row from ${fromIndex} to ${toIndex}`); }); ``` #### React ```tsx import '@toolbox-web/grid-react/features/row-drag-drop'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'priority', header: '#', type: 'number', width: 50 }, { field: 'task', header: 'Task' }, { field: 'status', header: 'Status' }, ], features: { rowDragDrop: true }, }; function ReorderableTaskList({ tasks }) { return ( <DataGrid rows={tasks} gridConfig={gridConfig} onRowMove={(detail) => console.log('Row moved:', detail)} style={{ height: '400px' }} /> ); } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `rowDragDrop` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/row-drag-drop'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const tasks = [ { priority: 1, task: 'Review PR', status: 'Pending' }, { priority: 2, task: 'Deploy staging', status: 'In Progress' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'priority', header: '#', type: 'number', width: 50 }, { field: 'task', header: 'Task' }, { field: 'status', header: 'Status' }, ], features: { rowDragDrop: true }, }; </script> <template> <TbwGrid :rows="tasks" :grid-config="gridConfig" style="height: 400px" @row-move="(e) => console.log('Row moved:', e.detail)" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `row-drag-drop` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/row-drag-drop'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-task-list', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" (rowMove)="onRowMove($event)" style="height: 400px; display: block;"> </tbw-grid> `, }) export class TaskListComponent { rows = [ { priority: 1, task: 'Review PR', status: 'Pending' }, { priority: 2, task: 'Deploy staging', status: 'In Progress' }, ]; gridConfig: GridConfig = { columns: [ { field: 'priority', header: '#', type: 'number', width: 50 }, { field: 'task', header: 'Task' }, { field: 'status', header: 'Status' }, ], features: { rowDragDrop: true }, }; onRowMove(detail: any) { console.log('Row moved:', detail); } } ``` :::note[Alternative: per-feature directive] You can instead import `GridRowDragDropDirective` and bind `[rowDragDrop]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Row Drag-Drop Plugin](https://toolboxjs.com/grid/plugins/row-drag-drop.md) --- # Selection Plugin > Cell, row, and range selection with full keyboard support, conditional selection, and checkbox mode. The Selection plugin adds cell, row, and range selection capabilities to the grid with full keyboard support. Whether you need simple cell highlighting or complex multi-range selections, this plugin has you covered. ## Installation ```ts import '@toolbox-web/grid/features/selection'; ``` ## Basic Usage #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/selection'; const data = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, { id: 3, name: 'Carol', email: 'carol@example.com' }, ]; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { selection: 'row', }, }; grid.rows = data; ``` #### React ```tsx import '@toolbox-web/grid-react/features/selection'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { selection: { mode: 'row' } }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `selection={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/selection'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { selection: 'row' }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:selection="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/selection'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { selection: 'row' }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridSelectionDirective` and bind `[selection]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Selection Plugin](https://toolboxjs.com/grid/plugins/selection.md) --- # Server-Side Plugin > Connect the grid to server-side data sources with virtual scrolling. The Server-Side plugin enables **virtual scrolling with lazy loading** from a remote API. It's designed for large datasets (10,000+ rows) where loading all data upfront would be impractical or slow. ## Installation ```ts import '@toolbox-web/grid/features/server-side'; ``` ## Basic Usage The feature requires a **data source** that implements the `getRows` method. Pass it directly in the config via `dataSource`: #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/server-side'; import type { GetRowsParams, ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; const dataSource: ServerSideDataSource = { async getRows(params: GetRowsParams) { // params: { startNode, endNode, sortModel, filterModel } const response = await fetch(`/api/data?start=${params.startNode}&end=${params.endNode}`); const data = await response.json(); return { rows: data.rows, totalNodeCount: data.total, // Required for scroll height calculation }; }, }; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { serverSide: { pageSize: 50, dataSource, }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/server-side'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; import type { GetRowsParams, ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; const dataSource: ServerSideDataSource = { async getRows(params: GetRowsParams) { const response = await fetch(`/api/data?start=${params.startNode}&end=${params.endNode}`); const data = await response.json(); return { rows: data.rows, totalNodeCount: data.total }; }, }; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { serverSide: { pageSize: 50, dataSource }, }, }; function ServerSideGrid() { return <DataGrid gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `serverSide={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup lang="ts"> import '@toolbox-web/grid-vue/features/server-side'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; import type { GetRowsParams, ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; const dataSource: ServerSideDataSource = { async getRows(params: GetRowsParams) { const response = await fetch(`/api/data?start=${params.startNode}&end=${params.endNode}`); const data = await response.json(); return { rows: data.rows, totalNodeCount: data.total }; }, }; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { serverSide: { pageSize: 50, dataSource }, }, }; </script> <template> <TbwGrid :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:server-side="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/server-side'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; import type { GetRowsParams, ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; @Component({ selector: 'app-server-side-grid', imports: [Grid], template: ` <tbw-grid [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class ServerSideGridComponent { dataSource: ServerSideDataSource = { getRows: async (params: GetRowsParams) => { const response = await fetch(`/api/data?start=${params.startNode}&end=${params.endNode}`); const data = await response.json(); return { rows: data.rows, totalNodeCount: data.total }; }, }; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { serverSide: { pageSize: 50, dataSource: this.dataSource }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridServerSideDirective` and bind `[serverSide]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Server-Side Plugin](https://toolboxjs.com/grid/plugins/server-side.md) --- # Shell Plugin > Wrap the grid with a header bar (title + toolbar) and a collapsible tool-panel sidebar. The Shell plugin wraps the grid with an optional **header bar** (title + toolbar) and a collapsible **tool panel** sidebar. Features like `visibility`, `filtering`, and `pivot` register their tool panels into this shell. :::note[Opt-in] The shell is opt-in and tree-shakeable. Enable it with `import '@toolbox-web/grid/features/shell'` + `features: { shell: true }` (or by registering `ShellPlugin` directly). Nothing shell-related is bundled unless you import it. ::: ## Installation ```ts import '@toolbox-web/grid/features/shell'; ``` The feature import registers the `ShellPlugin` and augments `features` with a typed `shell` option. Shell configuration types (`ShellConfig`, `ToolPanelDefinition`, …) are exported from `@toolbox-web/grid/plugins/shell`. ## Basic Usage Enable the shell by setting `features: { shell }` with a `header.title`. Features like `visibility` automatically register a tool panel when the shell is active. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/shell'; import '@toolbox-web/grid/features/visibility'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { features: { shell: { header: { title: 'Employee Data' } }, visibility: true, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/visibility'; import '@toolbox-web/grid-react/features/shell'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { features: { shell: { header: { title: 'Employee Data' } }, visibility: true, }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature props] You can instead pass `visibility` and `shell={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup lang="ts"> import '@toolbox-web/grid-vue/features/visibility'; import '@toolbox-web/grid-vue/features/shell'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const gridConfig: GridConfig = { features: { shell: { header: { title: 'Employee Data' } }, visibility: true, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: feature props] You can instead bind `visibility` and `:shell="{ … }"` directly on `<TbwGrid>`. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/visibility'; import '@toolbox-web/grid-angular/features/shell'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-shell-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class ShellGridComponent { rows = []; gridConfig: GridConfig = { features: { shell: { header: { title: 'Employee Data' } }, visibility: true, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridVisibilityDirective` and bind `[visibility]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: ```ts // ShellBasicDemo.astro import '@toolbox-web/grid'; import type { ColumnConfig } from '@toolbox-web/grid'; import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/shell'; import '@toolbox-web/grid/features/visibility'; import type { ShellConfig } from '@toolbox-web/grid/plugins/shell'; const container = document.getElementById('shell-basic-demo-container'); const grid = queryGrid('#demo-shell-basic'); if (container && grid) { const departments = ['Engineering', 'Sales', 'Marketing', 'Support']; const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry']; function generateRows(count: number) { return Array.from({ length: count }, (_, i) => ({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), active: i % 3 !== 0, })); } const shellColumns: ColumnConfig[] = [ { field: 'id', header: 'ID', type: 'number', width: 80 }, { field: 'name', header: 'Name', minWidth: 150 }, { field: 'department', header: 'Department', width: 150 }, { field: 'salary', header: 'Salary', type: 'number', width: 120 }, { field: 'active', header: 'Active', type: 'boolean', width: 80 }, ]; const sampleData = generateRows(20); interface ShellValues { showTitle: boolean; showHeaderContent: boolean; showToolbarButton: boolean; showVisibilityPlugin: boolean; showCustomPanel: boolean; panelPosition: string; panelMode: string; } async function rebuild(v: ShellValues) { const shellConfig: ShellConfig = { header: v.showTitle ? { title: 'Employee Data' } : {}, toolPanel: { position: v.panelPosition as 'left' | 'right', mode: v.panelMode as 'overlay' | 'push' | 'dropdown', }, }; grid.gridConfig = { columns: shellColumns, features: { shell: shellConfig, ...(v.showVisibilityPlugin ? { visibility: true } : {}), }, }; grid.rows = sampleData; // Plugins are instantiated on the first render, so the shell does not exist // synchronously after assigning `gridConfig`. await grid.ready(); const shell = grid.getPluginByName('shell'); if (v.showHeaderContent) { shell?.registerHeaderContent({ id: 'row-count', order: 10, render: (el) => { const span = document.createElement('span'); span.style.cssText = 'font-size:13px;color:var(--sl-color-gray-2);padding:4px 8px;background:var(--sl-color-gray-6);border-radius:4px;'; const update = () => { span.textContent = `${grid.rows.length} rows`; }; const unsub = grid.on('data-change', update); el.appendChild(span); return () => { unsub(); span.remove(); }; }, }); } else { shell?.unregisterHeaderContent('row-count'); } if (v.showCustomPanel) { shell?.registerToolPanel({ id: 'custom-info', title: 'Info Panel', icon: 'ℹ', tooltip: 'Info Panel', render: (el) => { el.innerHTML = '<div style="padding:16px;"><h4 style="margin:0 0 8px;">Custom Panel</h4><p style="margin:0;font-size:13px;">This panel was added via registerToolPanel().</p></div>'; return () => { el.innerHTML = ''; }; }, }); } else { shell?.unregisterToolPanel('custom-info'); } if (v.showToolbarButton) { shell?.registerToolbarContent({ id: 'refresh-btn', render: (el) => { const btn = document.createElement('button'); btn.className = 'tbw-toolbar-btn'; btn.title = 'Refresh Data'; btn.setAttribute('aria-label', 'Refresh Data'); btn.textContent = '↻'; btn.addEventListener('click', () => { grid.rows = generateRows(20); }); el.appendChild(btn); return () => btn.remove(); }, }); } else { shell?.unregisterToolbarContent('refresh-btn'); } } void rebuild({ showTitle: true, showHeaderContent: true, showToolbarButton: true, showVisibilityPlugin: true, showCustomPanel: false, panelPosition: 'right', panelMode: 'overlay', }); container.addEventListener('control-change', ((e: CustomEvent) => { void rebuild(e.detail.allValues as ShellValues); }) as EventListener); } ``` **Full plugin API — configuration options, events, methods, and advanced usage:** [Shell Plugin](https://toolboxjs.com/grid/plugins/shell.md) --- # Sticky Rows Plugin > Pin selected data rows below the header as the user scrolls past them. The **Sticky Rows** plugin keeps selected data rows pinned just under the header while the user scrolls past them. Useful for section markers, group headers in flat lists, "you are here" anchors, and any scenario where you want a row to remain visible after it would naturally scroll out of view. Stuck rows are **clones** of the real rows — the originals stay in the data flow, remain interactive, and continue to participate in keyboard navigation. Clones are decorative, marked `aria-hidden="true"`, and inherit the row's column-template alignment so they line up perfectly with the data below. ## Installation ```ts import '@toolbox-web/grid/features/sticky-rows'; ``` Or use the plugin directly: ```ts import { StickyRowsPlugin } from '@toolbox-web/grid/plugins/sticky-rows'; ``` ## Basic Usage Provide an `isSticky` predicate — either the **name of a boolean field** on your row data, or a **function** receiving `(row, index)` and returning a truthy value when the row should be sticky. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/sticky-rows'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'label', header: 'Label' }, { field: 'value', header: 'Value' }, ], features: { // Field-name shorthand: any row whose `isSection` is truthy is sticky. stickyRows: { isSticky: 'isSection' }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/sticky-rows'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'label', header: 'Label' }, { field: 'value', header: 'Value' }, ], features: { stickyRows: { isSticky: 'isSection' } }, }; function MyGrid({ rows }) { return <DataGrid rows={rows} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `stickyRows={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/sticky-rows'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const gridConfig: GridConfig = { columns: [ { field: 'label', header: 'Label' }, { field: 'value', header: 'Value' }, ], features: { stickyRows: { isSticky: 'isSection' } }, }; </script> <template> <TbwGrid :rows="rows" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:sticky-rows="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/sticky-rows'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [/* ... */]; gridConfig: GridConfig = { columns: [ { field: 'label', header: 'Label' }, { field: 'value', header: 'Value' }, ], // Field-name shorthand: any row whose `isSection` is truthy is sticky. features: { stickyRows: { isSticky: 'isSection' } }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridStickyRowsDirective` and bind `[stickyRows]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Sticky Rows Plugin](https://toolboxjs.com/grid/plugins/sticky-rows.md) --- # Tooltip Plugin > Display popover tooltips for truncated header and cell text, with per-column overrides. The Tooltip plugin shows popover tooltips when header or cell text overflows its container. It uses the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) with CSS anchor positioning for a native, consistent experience across browsers — with a JavaScript fallback for older browsers. Tooltips appear automatically on hover. Per-column overrides let you provide custom static text or dynamic content derived from row data. ## Installation ```ts import '@toolbox-web/grid/features/tooltip'; ``` ## Basic Usage #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; import '@toolbox-web/grid/features/tooltip'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email Address' }, { field: 'department', header: 'Department / Division' }, ], features: { tooltip: true }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/tooltip'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email Address' }, ], features: { tooltip: true }, }; function App({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `tooltip` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/tooltip'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email Address' }, ], features: { tooltip: true }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `tooltip` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/tooltip'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class GridComponent { rows = []; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email Address' }, ], features: { tooltip: true }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridTooltipDirective` and bind `[tooltip]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Tooltip Plugin](https://toolboxjs.com/grid/plugins/tooltip.md) --- # Tree Plugin > Display hierarchical data as an expandable tree. The Tree plugin transforms your flat grid into a hierarchical tree view with expandable parent-child relationships. Great for file explorers, organizational charts, nested categories, or any data with a natural hierarchy. ## Installation ```ts import '@toolbox-web/grid/features/tree'; ``` ## Basic Usage Just point the feature at the field containing child items (defaults to `children`) and the grid handles all the expand/collapse behavior, indentation, and icons automatically. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' } ], features: { tree: { childrenField: 'children', indentWidth: 24, }, }, }; grid.rows = [ { id: 1, name: 'Documents', type: 'folder', children: [ { id: 2, name: 'Work', type: 'folder', children: [ { id: 3, name: 'Report.docx', type: 'file', size: '24 KB' } ]}, { id: 4, name: 'Personal', type: 'folder', children: [] } ] }, ]; ``` #### React ```tsx import '@toolbox-web/grid-react/features/tree'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' } ], features: { tree: { childrenField: 'children', indentWidth: 24 }, }, }; function FileExplorer({ files }) { return <DataGrid rows={files} gridConfig={gridConfig} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `tree={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/tree'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const files = [ { id: 1, name: 'Documents', type: 'folder', children: [ { id: 2, name: 'Work', type: 'folder', children: [{ id: 3, name: 'Report.docx', type: 'file', size: '24 KB' }] }, { id: 4, name: 'Personal', type: 'folder', children: [] }, ], }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' }, ], features: { tree: { childrenField: 'children', indentWidth: 24 }, }, }; </script> <template> <TbwGrid :rows="files" :grid-config="gridConfig" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `:tree="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it. ```typescript import '@toolbox-web/grid-angular/features/tree'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-file-explorer', imports: [Grid], template: ` <tbw-grid [rows]="files" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class FileExplorerComponent { files = [/* hierarchical data */]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' } ], features: { tree: { childrenField: 'children', indentWidth: 24 }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridTreeDirective` and bind `[tree]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Tree Plugin](https://toolboxjs.com/grid/plugins/tree.md) --- # Undo/Redo Plugin > Add undo/redo support for cell edits. The Undo/Redo plugin tracks all cell edits and lets users revert or replay changes with familiar keyboard shortcuts (Ctrl+Z / Ctrl+Y). It maintains an in-memory history stack with configurable depth—perfect for data entry workflows where mistakes happen. > ⚠️ **Required Dependency:** This plugin requires [EditingPlugin](https://toolboxjs.com/grid/plugins/editing.md) to be loaded first. UndoRedo tracks the edit history that EditingPlugin creates. ## Installation ```ts import '@toolbox-web/grid/features/editing'; import '@toolbox-web/grid/features/undo-redo'; ``` ## Basic Usage Enable both features and history tracking is automatic—every cell edit is recorded, and users can navigate back and forth through the edit history. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'quantity', header: 'Quantity', type: 'number', editable: true }, ], features: { editing: 'dblclick', undoRedo: { maxHistorySize: 50 }, }, }; ``` #### React ```tsx import '@toolbox-web/grid-react/features/editing'; import '@toolbox-web/grid-react/features/undo-redo'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'quantity', header: 'Quantity', type: 'number', editable: true }, ], features: { editing: { editOn: 'dblclick' }, undoRedo: { maxHistorySize: 50 }, }, }; function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}`, `editing={{ … }}` and `undoRedo={{ … }}` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/editing'; import '@toolbox-web/grid-vue/features/undo-redo'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { name: 'Widget', price: 9.99, quantity: 10 }, { name: 'Gadget', price: 19.99, quantity: 5 }, ]; const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'quantity', header: 'Quantity', type: 'number', editable: true }, ], features: { editing: 'dblclick', undoRedo: { maxHistorySize: 50 }, }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead bind `editing="…"` and `:undo-redo="{ … }"` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular Define everything inside `gridConfig.features`, exactly like the other frameworks. The side-effect imports register the features so the adapter can bridge them. ```typescript import '@toolbox-web/grid-angular/features/editing'; import '@toolbox-web/grid-angular/features/undo-redo'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class MyGridComponent { rows = [...]; gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'price', header: 'Price', type: 'number', editable: true }, { field: 'quantity', header: 'Quantity', type: 'number', editable: true }, ], features: { editing: true, undoRedo: { maxHistorySize: 50 }, }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridEditingDirective` and `GridUndoRedoDirective` and bind `[editing]` / `[undoRedo]` directly on `<tbw-grid>`. That form gives a compile-time check that the features are imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Undo/Redo Plugin](https://toolboxjs.com/grid/plugins/undo-redo.md) --- # Column Visibility Plugin > Allow users to toggle column visibility via a panel. The Visibility plugin gives users control over which columns are displayed. Hide less important columns by default, let users toggle them via a column chooser UI, or programmatically show/hide columns based on user preferences or screen size. > 💡 **Optional Enhancement:** When [ReorderPlugin](https://toolboxjs.com/grid/plugins/reorder-columns.md) is also loaded, columns in the visibility panel become draggable for reordering. When [Column Grouping](https://toolboxjs.com/grid/plugins/grouping-columns.md) is also active, the panel shows columns in their actual display order, and fragmented groups (split across non-contiguous positions) appear as separate panel sections. Group headers in the panel are draggable and move only the columns in that fragment. ## Installation ```ts import '@toolbox-web/grid/features/visibility'; ``` ## Basic Usage Set `hidden: true` on columns you want hidden by default. The feature provides a UI and API for toggling column visibility at runtime. #### TypeScript ```ts import { queryGrid } from '@toolbox-web/grid'; const grid = queryGrid('tbw-grid'); grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'phone', header: 'Phone', hidden: true }, // Hidden by default { field: 'address', header: 'Address', hidden: true }, ], features: { visibility: true }, }; // Toggle visibility programmatically const plugin = grid.getPluginByName('visibility'); plugin.showColumn('phone'); plugin.hideColumn('email'); ``` #### React ```tsx import '@toolbox-web/grid-react/features/visibility'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig } from '@toolbox-web/grid-react'; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'phone', header: 'Phone', hidden: true }, ], features: { visibility: true }, }; function ConfigurableGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />; } ``` :::note[Alternative: feature prop] You can instead pass `columns={[ … ]}` and `visibility` as individual props on `<DataGrid>`. That form is idiomatic React, but `gridConfig` keeps the configuration identical across every framework. ::: #### Vue ```html <script setup> import '@toolbox-web/grid-vue/features/visibility'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig } from '@toolbox-web/grid-vue'; const data = [ { id: 1, name: 'Alice', email: 'alice@example.com', phone: '555-1234' }, { id: 2, name: 'Bob', email: 'bob@example.com', phone: '555-5678' }, ]; const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'phone', header: 'Phone', hidden: true }, ], features: { visibility: true }, }; </script> <template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /> </template> ``` :::note[Alternative: prop + light-dom columns] You can instead set `visibility` and declare columns with `<TbwGridColumn>` children. That form is idiomatic Vue, but `gridConfig` keeps the configuration identical across every framework. ::: #### Angular ```typescript import '@toolbox-web/grid-angular/features/visibility'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-configurable-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `, }) export class ConfigurableGridComponent { rows = []; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'phone', header: 'Phone', hidden: true }, ], features: { visibility: true }, }; } ``` :::note[Alternative: per-feature directive] You can instead import `GridVisibilityDirective` and bind `[visibility]` directly on `<tbw-grid>`. That form gives a compile-time check that the feature is imported, but `gridConfig.features` above is the recommended, framework-consistent approach. ::: **Full plugin API — configuration options, events, methods, and advanced usage:** [Column Visibility Plugin](https://toolboxjs.com/grid/plugins/visibility.md) --- # Recipes > Task-oriented how-to guides for @toolbox-web/grid — editable grids, streaming data, scroll-driven UI, PDF export, spreadsheet formulas, in-cell charts, and input masking. Feature pages tell you what each option does. Recipes answer the other question: **"how do I build _this_?"** Each one starts from a concrete goal and ends with runnable code and the failure modes we know about. ## Recipe shape Every page follows the same four steps, so you can skim straight to the part you need: 1. **Problem** — the concrete thing you're building. 2. **What you need** — which grid features to enable, plus any external library and its approximate size. 3. **Code** — runnable code against public, versioned API. 4. **Caveats** — the failure modes we know about. Recipes are written in vanilla TypeScript. `gridConfig` is byte-identical across React, Vue, and Angular, so the only thing that changes per framework is how you hand the config to the component and how you bind events — see [Framework Adapters](https://toolboxjs.com/grid/framework-adapters.md) and [Listening to Events](https://toolboxjs.com/grid/api-reference.md#listening-to-events). ## Using the grid Built entirely from `@toolbox-web/grid` — supported, versioned, and covered by our tests. - [Editable grid with undo](https://toolboxjs.com/grid/recipes/editable-grid-with-undo.md): Inline editing with undo/redo, dirty tracking, and validation that rejects a bad value. - [Real-time streaming data](https://toolboxjs.com/grid/recipes/real-time-streaming.md): Feed WebSocket or SSE deltas into the grid with row transactions. - [Scroll & render driven UI](https://toolboxjs.com/grid/recipes/scroll-and-render.md): Infinite scroll, lazy cell hydration, and focusing a row you just added. ## Integrating a third-party library These reach a capability the grid deliberately does not ship. The grid holds a hard bundle budget — core stays under 50 kB gzipped, and anything that can be a plugin is a plugin. A PDF writer is larger than the entire grid; a formula engine is larger still. So rather than ship a mediocre built-in, the grid exposes the **seam** you can drive an external library from: | Capability | Seam | | --- | --- | | PDF export | `ExportPlugin.getResolvedColumns()` / `.export()` | | Computed & formula columns | `column.valueAccessor` | | Charts and rich cell content | `column.renderer` | | Masked, validated or exotic input | `column.editor` + the `cell-commit` event | - [PDF export](https://toolboxjs.com/grid/recipes/pdf-export.md): Drive pdfmake or jsPDF + AutoTable from the export plugin - [Spreadsheet formulas](https://toolboxjs.com/grid/recipes/spreadsheet-formulas.md): Wire HyperFormula through valueAccessor for Excel-compatible computed columns. - [In-cell sparklines](https://toolboxjs.com/grid/recipes/sparklines.md): Trend lines in a cell with inline SVG, escalating to uPlot when you need axes. - [Input masks & validation](https://toolboxjs.com/grid/recipes/input-masks-validation.md): IMask editors and Zod schema validation wired to the grid :::caution[Third-party recipes are not supported integrations] We don't test against these libraries, track their releases, or pin their versions. Treat the code as a starting point you own, not a dependency of the grid. The grid-side API every recipe uses **is** public and versioned — if a recipe needs something that isn't, that's a bug, please [open an issue](https://github.com/OysteinAmundsen/toolbox/issues). ::: --- # Editable grid with undo > Inline cell editing with undo/redo, dirty tracking, and validation that rejects a bad value before it is committed. **Problem:** users edit cells inline, can undo mistakes with `Ctrl+Z`, and a Save button stays disabled until something actually changed — with invalid values rejected before they reach your data. ## What you need Three built-in features, no external library: | Feature | Why | | --- | --- | | `editing` | The inline editors themselves, plus dirty tracking | | `undoRedo` | `Ctrl+Z` / `Ctrl+Shift+Z` history | | `selection: 'cell'` | Gives the keyboard a cell to start editing from | ## Code ```typescript import '@toolbox-web/grid/features/editing'; import '@toolbox-web/grid/features/undo-redo'; import '@toolbox-web/grid/features/selection'; grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name', editable: true }, { field: 'email', header: 'Email', editable: true }, { field: 'active', header: 'Active', type: 'boolean', editable: true }, ], features: { editing: { editOn: 'dblclick', dirtyTracking: true }, undoRedo: true, selection: 'cell', }, getRowId: (row) => row.id, // Required — dirty tracking is keyed by row ID }; // Reject an invalid value before it is written back to the row grid.on('cell-commit', (detail, e) => { if (detail.field === 'email' && !detail.value.includes('@')) { e.preventDefault(); } }); // Enable Save only while there is something to save grid.on('dirty-change', () => { const editing = grid.getPluginByName('editing'); saveButton.disabled = (editing?.getDirtyRows() ?? []).length === 0; }); ``` The `gridConfig` above is identical in React, Vue, and Angular — see [Framework Adapters](https://toolboxjs.com/grid/framework-adapters.md) for how to hand it to each component, and [Listening to Events](https://toolboxjs.com/grid/api-reference.md#listening-to-events) for the per-framework event binding syntax. ## Caveats - **`getRowId` is not optional here.** Dirty tracking and undo both identify rows by ID, not by index. Without it, sorting or filtering makes the grid lose track of which row was edited. - **`cell-commit` is cancelable, `cell-change` is not.** Validate in `cell-commit` and call `preventDefault()`; by the time `cell-change` fires the value is already committed. - **`preventDefault()` reverts the cell**, it does not keep the editor open. To keep a bad value visible while flagging it, use [cell validation](https://toolboxjs.com/grid/plugins/editing.md#cell-validation) instead. - **Feature dependencies resolve automatically.** If you drop to the plugin API directly, `EditingPlugin` must be registered **before** `UndoRedoPlugin`. - **Undo history is in-memory** and is cleared when `rows` is replaced wholesale. --- # Real-time streaming data > Push live WebSocket, SSE, or polling updates into the grid with row transactions, without re-assigning the whole dataset. **Problem:** a WebSocket, SSE stream, or poll loop delivers row-level deltas, and re-assigning `grid.rows` on every message would rebuild the entire row model and destroy scroll position and selection. ## What you need No external library and no plugin — `applyTransaction()` and `applyTransactionAsync()` are core grid API. See [Row Transactions](https://toolboxjs.com/grid/core.md#row-transactions) for the payload shape and the sync/async trade-off. ## Code ```typescript import { createGrid } from '@toolbox-web/grid'; const grid = createGrid<Trade>('#my-grid'); // Low-to-moderate frequency: one transaction per message 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, }); }; ``` For a high-frequency ticker, switch to the async form — it merges every call made within the same animation frame into a single render: ```typescript ws.onmessage = (e) => { const msg = JSON.parse(e.data); grid.applyTransactionAsync({ update: [{ id: msg.id, changes: msg.changes }], }); }; ``` ## Caveats - **`getRowId` is required.** `update` and `remove` match by row ID; without `getRowId` the grid has nothing to match against. - **`applyTransactionAsync()` disables row animations** by design — flashing every cell at 100 msg/s is noise, not feedback. Use the sync form when you want the change highlight. - **Operations apply in a fixed order** (removes → updates → adds), so a single transaction can safely remove and re-add the same ID. - **`add` appends to the end** of the row array. If the grid is sorted, the new row jumps to its sorted position on the next render — don't assume it lands where you appended it. - **Both methods return a `TransactionResult`** with the actual row objects touched. Use it for logging rather than re-deriving what changed. - **Over ~50k rows**, consider [ServerSidePlugin](https://toolboxjs.com/grid/plugins/server-side.md) instead of streaming the full dataset into the client. --- # Scroll & render driven UI > Infinite scroll, lazy cell hydration, and focusing a freshly added row using the grid's tbw-scroll and render events. **Problem:** you need to react to the grid's viewport — load the next page as the user nears the bottom, hydrate expensive cell content only when visible, or touch the DOM of a row you just added programmatically. ## What you need Two core events, no plugin: | Event | Fires | | --- | --- | | `tbw-scroll` | Once per animation frame while the vertical viewport scrolls | | `render` | Once at the end of every render flush, after all plugin `afterRender` hooks | `tbw-scroll` carries `scrollTop`, `scrollHeight`, `clientHeight`, and a `direction: 'vertical'` discriminator. The detail is a fresh object literal each tick, so it is safe to retain, freeze, copy into framework state, or post to a worker. ## Code ### Infinite scroll / load more ```typescript grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => { if (scrollTop + clientHeight >= scrollHeight - 200) { loadNextPage(); } }); ``` ### Sticky scroll-progress indicator For a progress bar living outside the grid: ```typescript grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => { const max = scrollHeight - clientHeight; progressBarEl.style.width = `${(max > 0 ? scrollTop / max : 0) * 100}%`; }); ``` ### Defer heavy cell content Mount charts, images, or embedded video only for rows near the viewport: ```typescript grid.on('tbw-scroll', ({ scrollTop, clientHeight }) => { hydrateHeavyCellsBetween(scrollTop, scrollTop + clientHeight); }); ``` ### Dismiss overlays on scroll Tooltips, popovers, and context menus rendered outside the grid should generally close when the viewport moves: ```typescript grid.on('tbw-scroll', () => closeOpenOverlays()); ``` ### Focus the first input after `addRow` With `editing: { mode: 'grid' }` every row is permanently in edit mode. After inserting a row you want its first cell focused — but that row does not exist in the DOM until the next render. The `render` event is the supported hook, so you never need a `setTimeout` or double-`requestAnimationFrame` hack: ```typescript function addEmployee() { grid.addRow({ id: crypto.randomUUID(), name: '', email: '' }); grid.addEventListener( 'render', () => { grid.querySelector<HTMLInputElement>('[data-row="0"][data-col="0"] input')?.focus(); }, { once: true }, ); } ``` ### Skip cheap scroll renders `render` also fires for virtualization-only re-renders. Gate on `phase` when you only care about row or column model changes: ```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`; }); ``` ## Caveats - **`tbw-scroll` is not per-row visibility.** It fires at most once per frame, not when a row enters or leaves the viewport. For "row N just became visible", observe rendered rows in `afterRowRender` or use an `IntersectionObserver` from a custom plugin. - **Prefer the plugin for pagination.** [ServerSidePlugin](https://toolboxjs.com/grid/plugins/server-side.md) already handles block fetching; `tbw-scroll` is the lower-level primitive for cases it doesn't cover. - **`ready()` is not the `render` event.** `grid.ready()` resolves once, after the first render; `render` fires on every flush. Attach with `{ once: true }` when you only care about one specific mutation. - **Pure visual effects may not need JS at all** — native `animation-timeline: scroll()` covers scroll-driven CSS on a separate scroller. - **Adapter event names are disambiguated** to avoid colliding with the native `scroll` event: | Adapter | Scroll | Render | | --- | --- | --- | | React | `<DataGrid onTbwScroll={...} />` | `<DataGrid onRender={...} />` | | Vue | `<TbwGrid @tbw-scroll="..." />` | `<TbwGrid @render="..." />` | | Angular | `<tbw-grid (tbwScroll)="..." />` | `<tbw-grid (render)="..." />` | --- # Plugin Development > Build plugins that extend @toolbox-web/grid — lifecycle hooks, manifests, communication, feature registration, and bundling. `@toolbox-web/grid` is **plugin-driven by design**. The core is intentionally minimal (rendering, virtualization, scheduling); selection, editing, filtering, grouping, drag-and-drop, validation, accessibility shortcuts — almost every grid capability is a plugin under `libs/grid/src/lib/plugins/`. The same APIs are available to you for building your own. This section covers the plugin system from three angles: - [Architecture](https://toolboxjs.com/grid/plugin-development/architecture.md): How the plugin system works internally — lifecycle, hooks, render integration, the feature registry, and tree-shaking. - [Authoring Guide](https://toolboxjs.com/grid/plugin-development/custom-plugins.md): Step-by-step guide to building your first plugin: BaseGridPlugin, manifest, communication, queries, testing, distribution. - [API Reference](https://toolboxjs.com/grid/api/plugin-development.md): Typed reference for BaseGridPlugin, GridPlugin, PluginManifest, hooks, and the feature registry. ## When to write a plugin Reach for a plugin when you need to: - React to grid lifecycle events (render, attach, detach, scroll, cell click, keyboard). - Add new column-level or grid-level configuration that the grid validates and merges. - Communicate with other plugins (e.g. an "audit log" plugin that listens for edits from `EditingPlugin`). - Ship a reusable behaviour as its own package — third-party plugins are first-class. For one-off styling or event handlers, plain CSS and `grid.addEventListener(...)` are usually enough — no plugin needed. --- # Plugin Architecture > How the @toolbox-web/grid plugin system works internally — lifecycle, hooks, communication, manifests, validated properties, and the feature registry. This page describes how the plugin and feature subsystems work inside `@toolbox-web/grid`. It is the contributor-facing companion to the [Authoring Guide](https://toolboxjs.com/grid/plugin-development/custom-plugins.md) and to the typed [`BaseGridPlugin` API reference](https://toolboxjs.com/grid/api/plugin-development/classes/basegridplugin.md). For grid-core internals (render scheduler, virtualization, DOM structure, configuration), see the main [Architecture](https://toolboxjs.com/grid/architecture.md) page. ## Plugin System ### Plugin Lifecycle ```mermaid flowchart TB A["new Plugin()<br/><i>Constructor stores user config</i>"] --> B["attach(grid)<br/><i>Plugin receives grid ref, merges config</i>"] B --> C["Grid Render Cycle"] C --> D["User Interaction"] D --> E["detach()<br/><i>Cleanup on grid disconnect</i>"] subgraph C["Grid Render Cycle"] C1["processColumns() hook"] C2["processRows() hook"] C3["afterCellRender() hook (per cell)"] C4["afterRowRender() hook (per row)"] C5["afterRender() hook"] end subgraph D["User Interaction"] D1["onCellClick() hook"] D2["onKeyDown() hook"] D3["onScroll() hook"] D4["onCellMouseDown/Move/Up()"] end ``` ### Creating a Plugin ```typescript import { BaseGridPlugin, CellClickEvent, type GridElement } from '@toolbox-web/grid'; import styles from './my-plugin.css?inline'; interface MyPluginConfig { enabled?: boolean; } export class MyPlugin extends BaseGridPlugin<MyPluginConfig> { readonly name = 'myPlugin'; override readonly styles = styles; protected override get defaultConfig(): Partial<MyPluginConfig> { return { enabled: true }; } override attach(grid: GridElement): void { super.attach(grid); // MUST call super // Setup listeners with this.disconnectSignal for auto-cleanup } override afterRender(): void { if (!this.config.enabled) return; // Access DOM via this.gridElement } override onCellClick(event: CellClickEvent): boolean | void { // Return true to prevent default behavior } } ``` ### Plugin Communication Plugins communicate via three channels: **Event Bus** (plugin-to-plugin notifications): ```typescript // Listen for events from other plugins this.on('selection-cleared', (detail) => { /* ... */ }); // Emit to other plugins only this.emitPluginEvent('selection-cleared', { source: 'keyboard' }); // Emit to both plugins AND external addEventListener consumers this.broadcast('sort-change', { sortModel: [...this.sortModel] }); ``` **Query System** (sync state retrieval): ```typescript // Declare queryable state in manifest static override readonly manifest = { queries: [{ type: 'canMoveColumn', description: 'Check movability' }], }; // Handle queries override handleQuery(query: PluginQuery): unknown { if (query.type === 'canMoveColumn') return !column.pinned; return undefined; } // Query from another plugin const responses = this.grid.query<boolean>('canMoveColumn', column); ``` ### Plugin Manifest Plugins declare owned config properties via a static `manifest`: ```typescript import type { PluginManifest } from '@toolbox-web/grid'; static override readonly manifest: PluginManifest<EditingConfig> = { ownedProperties: [ { property: 'editable', level: 'column', description: 'the "editable" column property' }, { property: 'editor', level: 'column', description: 'the "editor" column property' }, ], configRules: [ { id: 'editing/invalid-edit-on', severity: 'warn', message: '"editOn: dblclick" has no effect when editable is false', check: (config) => config.editOn === 'dblclick' && config.editable === false, }, ], }; ``` See the [Custom Plugins guide → Plugin Manifest](https://toolboxjs.com/grid/plugin-development/custom-plugins.md#plugin-manifest) for the full schema (queries, events, incompatibleWith, hookPriority). The grid validates at runtime that properties like `editable: true` are only used when the owning plugin is loaded, and provides helpful error messages with import hints. ### Validated Properties | Property | Required Plugin | Level | | -------------- | ----------------------- | ------ | | `editable` | `EditingPlugin` | Column | | `editor` | `EditingPlugin` | Column | | `editorParams` | `EditingPlugin` | Column | | `group` | `GroupingColumnsPlugin` | Column | | `pinned` | `PinnedColumnsPlugin` | Column | | `columnGroups` | `GroupingColumnsPlugin` | Config | Validation runs in the `RenderScheduler.mergeConfig` callback, **after** plugins are initialized. Error messages clearly state which plugin is missing and how to import it. ### Hook performance budget Every plugin hook runs in the grid's hot path. Keep this table in mind when implementing them — work that's cheap per cell becomes expensive when you multiply it by the number of visible cells and the frequency of scroll events. | Hook | When it runs | Impact | Rule of thumb | |------|-------------|--------|---------------| | `processColumns()` | Every data/config update | Low | Runs once per update — fine for non-trivial work | | `processRows()` | Every data update | Medium | Runs over full dataset — `O(n)` work is OK, `O(n²)` is not | | `afterCellRender()` | Every visible cell, every scroll frame | **High** | Keep < 0.1 ms per call; no DOM queries, no allocations | | `afterRowRender()` | Every visible row, every scroll frame | **High** | Same budget as cells; cache anything reusable in `processRows()` | | `afterRender()` | Every render cycle | Medium | Avoid DOM queries; use cached refs from earlier hooks | **Profiling slow plugins:** open Chrome DevTools → Performance, record a scroll, and look for your plugin name in the flame chart. Work that consistently shows up in `afterCellRender` / `afterRowRender` should usually move to `processRows()` and be cached. ### Style injection Plugins must not append `<style>` elements as children of `<tbw-grid>` — they get removed by `replaceChildren()` on the next render. Use one of: ```ts // ✅ The `styles` property (recommended for plugins — adopted into the grid's stylesheets) override readonly styles = ` .my-class { color: blue; } `; // ✅ registerStyles for runtime-injected / dynamic CSS this.gridElement.registerStyles('my-id', '.my-class { color: blue; }'); // ✅ Standard global CSS also works (stylesheet, <style> in <head>) // ❌ Child <style> nodes inside the grid are wiped on render const style = document.createElement('style'); this.gridElement.appendChild(style); ``` ### Wrapping plugins (host-DOM chrome) Most plugins **enrich** the grid: they tag rows in `processRows()`, decorate cells in `afterCellRender()`, or handle interaction hooks. A few plugins instead **wrap** the grid — they take the freshly built grid DOM and render their own chrome (a header bar, a sidebar, a toolbar) _around_ it. The [Shell plugin](https://toolboxjs.com/grid/plugins/shell.md) is the canonical example. Wrapping is not done with the per-cell/per-row hooks. It uses one dedicated lifecycle hook: ```typescript override afterStructuralRender(): void { const root = this.grid?._renderRoot; if (!root || root.querySelector('.my-chrome > .tbw-grid-content')) return; // 1. Core has built a BARE grid containing `.tbw-grid-content`. // 2. Relocate that existing node into your chrome — do NOT re-create it. // Moving (not rebuilding) preserves the content subtree, its event // listeners, and the grid's cached DOM refs. const content = root.querySelector('.tbw-grid-content'); const chrome = buildChrome(); // header bar + body + sidebar, etc. chrome.querySelector('[data-content-slot]')!.append(content!); root.append(chrome); } ``` Key contract for a wrapping plugin: - **Use `afterStructuralRender()`, not `afterRender()`.** It fires synchronously inside the same task as the structural DOM build, before paint — so there is no flash of unwrapped content. It runs only on full structural rebuilds (connect / structural change), **never** on the scroll or data hot path. - **Move the `.tbw-grid-content` node; never replace it.** The grid caches references into that subtree (body, viewport, rows). Re-creating it breaks scrolling, virtualization, and every plugin that cached a ref. Relocate the existing element into your wrapper. - **Be idempotent.** A full rebuild discards whatever chrome a previous invocation produced, so re-create it each time and early-return when the desired structure is already present. - **Render your own content after wrapping.** Inject titles, toolbar buttons, and panel bodies into the chrome you built — query them from `this.grid._renderRoot`, not from the grid-body refs (which point inside `.tbw-grid-content`). See [`afterStructuralRender` in the `BaseGridPlugin` reference](https://toolboxjs.com/grid/api/plugin-development/classes/basegridplugin.md) for the full hook contract. --- ## PluginManager `PluginManager` owns the attached plugin instances and dispatches every lifecycle hook to them. The grid creates one per element; you never construct it yourself. Reach it from a plugin via `this.grid._pluginManager`: ```ts import { PluginManager } from '@toolbox-web/grid/all'; ``` Most of its surface is hook dispatch that the grid calls for you. The methods worth knowing when writing a plugin are: | Method | Returns | Purpose | | ------------------------------- | -------------------------------- | -------------------------------------------------- | | `getPlugin(PluginClass)` | `T \| undefined` | Look up a sibling plugin by constructor. | | `getPluginByName(name)` | `BaseGridPlugin \| undefined` | Look up a sibling plugin by its registered name. | | `hasPlugin(PluginClass)` | `boolean` | Test for a sibling plugin without retrieving it. | | `getAll()` / `getPlugins()` | `readonly BaseGridPlugin[]` | All attached plugins, in attachment order. | | `getRegisteredPluginNames()` | `string[]` | Names of all attached plugins. | | `queryPlugins(query)` | `T[]` | Capability-based lookup — see below. | | `subscribe(plugin, type, cb)` | `void` | Listen for another plugin's events. | | `unsubscribe(plugin, type)` | `void` | Stop listening for one event type. | | `unsubscribeAll(plugin)` | `void` | Drop all of a plugin's subscriptions. | | `emitPluginEvent(type, detail)` | `void` | Broadcast to subscribed plugins (not the DOM). | | `getCellRenderer(type)` | `CellRenderer \| undefined` | Resolve a plugin-registered renderer by type name. | | `getHeaderRenderer(type)` | `HeaderRenderer \| undefined` | Resolve a plugin-registered header renderer. | | `getCellEditor(type)` | `CellEditor \| undefined` | Resolve a plugin-registered editor. | Prefer `BaseGridPlugin`'s `on`, `off`, and `emitPluginEvent` helpers over calling `subscribe`/`emitPluginEvent` directly — they clean up automatically on detach. Grid consumers should use [`grid.getPlugin()`](https://toolboxjs.com/grid/api-reference.md) and `grid.getPluginByName()` instead of reaching for the manager. --- ## Feature Registry The **features API** is the recommended way to enable grid capabilities. It wraps the plugin system with declarative configuration and tree-shakeable side-effect imports. ### Why Features? | Aspect | Features (recommended) | Plugins (advanced) | |--------|----------------------|--------------------| | API | `features: { selection: 'row' }` | `plugins: [new SelectionPlugin({ mode: 'row' })]` | | Import | `import '@toolbox-web/grid/features/selection'` | `import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection'` | | Dependencies | Auto-resolved | Manual ordering | | Tree-shaking | Zero cost if unused | Must avoid importing unused plugins | ### How It Works Each feature module is a side-effect import that registers a factory function: ```mermaid flowchart LR subgraph imports["SIDE-EFFECT IMPORTS"] A["import '.../features/selection'"] B["import '.../features/editing'"] end A -->|"module executes"| C["registerFeature('selection', factory)"] B -->|"module executes"| D["registerFeature('editing', factory)"] C --> E["featureRegistry<br/><i>Map<string, factory></i>"] D --> E F["gridConfig.features"] --> G["createPluginsFromFeatures()"] E --> G G --> H["Plugin instances<br/>(ordered by dependencies)"] ``` ### Registration When a feature module is imported, it runs immediately at load time: ```typescript // libs/grid/src/lib/features/selection.ts import { SelectionPlugin } from '../plugins/selection'; import { registerFeature } from './registry'; registerFeature('selection', (config) => { // Shorthand strings → full config if (config === 'cell' || config === 'row' || config === 'range') { return new SelectionPlugin({ mode: config }); } return new SelectionPlugin(config ?? undefined); }); ``` Each factory handles **shorthand values** (e.g., `'row'` → `{ mode: 'row' }`), so users get a simpler API. ### Lazy Hook Design The grid core never references the feature registry directly. Instead, a **hook function** bridges them: ```typescript // feature-hook.ts — imported by grid core export let resolveFeatures: FeatureResolverFn | undefined; // Starts undefined! // registry.ts — imported only when a feature is imported setFeatureResolver(createPluginsFromFeatures); // Sets the hook ``` If no feature modules are imported, `resolveFeatures` stays `undefined` — the entire registry module is tree-shaken away by the bundler. This is why features are zero-cost when unused. ### Resolution Flow During plugin initialization: ```typescript // grid.ts — #initializePlugins() const features = this.#effectiveConfig?.features; if (features && resolveFeatures) { featurePlugins = resolveFeatures(features); } // Feature plugins ordered first, then explicit plugins const allPlugins = [...featurePlugins, ...explicitPlugins]; ``` Dependencies are auto-ordered: `selection` and `editing` always instantiate first, since other plugins (like `clipboard` or `undoRedo`) depend on them. ### Tree-Shaking Feature modules are marked as side-effects in `package.json`: ```json { "sideEffects": ["./lib/features/*.js"] } ``` This tells bundlers: "these imports have side effects (registration), don't optimize them away." But un-imported features are never loaded, so only the features you use add to bundle size (~200–300 bytes each). --- # Custom Plugins > Build custom plugins for @toolbox-web/grid — lifecycle hooks, communication, queries, manifests, testing, and complete examples. Learn how to extend `@toolbox-web/grid` with your own plugins. Plugins can add new features, modify data, inject styles, and respond to user interactions. ## Plugin Architecture All plugins extend [`BaseGridPlugin`](https://toolboxjs.com/grid/api/plugin-development/classes/basegridplugin.md) and implement lifecycle hooks: ```mermaid flowchart TB A["attach()<br/><i>Called when plugin is added to grid</i>"] B["processColumns()<br/><i>Transform column definitions</i>"] C["processRows()<br/><i>Transform row data</i>"] subgraph render["For each visible row"] direction TB D["afterCellRender()<br/><i>Per-cell hook (called for each cell)</i>"] E["afterRowRender()<br/><i>Per-row hook (after all cells in row)</i>"] D --> E end F["afterRender()<br/><i>Grid-wide DOM manipulation</i>"] G["User Interactions<br/><i>onCellClick, onKeyDown, etc.</i>"] H["detach()<br/><i>Cleanup when plugin is removed</i>"] A --> B --> C --> render --> F --> G --> H ``` ## Basic Plugin Structure A typical plugin lives in a folder under `libs/grid/src/lib/plugins/`: - my-feature/ - my-feature-plugin.ts — Plugin class extending [`BaseGridPlugin`](https://toolboxjs.com/grid/api/plugin-development/classes/basegridplugin.md) - my-feature-plugin.spec.ts — Co-located unit tests - my-feature.css — Plugin styles (optional) - types.ts — Public config/event types (optional) - index.ts — Barrel export ```typescript import { BaseGridPlugin, type AfterCellRenderContext } from '@toolbox-web/grid'; // 1. Define your config interface interface HighlightConfig { field: string; threshold: number; className?: string; } // 2. Extend BaseGridPlugin with your config type export class HighlightPlugin extends BaseGridPlugin<HighlightConfig> { // Required: unique plugin name readonly name = 'highlight'; // Optional: CSS styles to inject override readonly styles = ` .highlight-cell { background: linear-gradient(135deg, #fff3cd, #ffeeba); font-weight: 600; } `; // Called for each cell during rendering override afterCellRender(context: AfterCellRenderContext): void { const { field, threshold, className = 'highlight-cell' } = this.config; const { column, value, cellElement } = context; if (column.field !== field) return; const numValue = typeof value === 'number' ? value : parseFloat(String(value)); if (!isNaN(numValue) && numValue > threshold) { cellElement.classList.add(className); } } } ``` ## Using Your Plugin ```typescript import { HighlightPlugin } from './highlight-plugin'; grid.gridConfig = { columns: [...], plugins: [ new HighlightPlugin({ field: 'salary', threshold: 100000, className: 'high-earner', }), ], }; ``` ## Lifecycle Hooks ### `attach(grid)` Called when the plugin is attached to the grid. Use for initial setup. ```typescript import type { GridElement } from '@toolbox-web/grid'; override attach(grid: GridElement): void { super.attach(grid); // Always call super first! this.state = new Map(); console.log('Attached to grid with', grid.rows.length, 'rows'); } ``` ### `detach()` Called when the plugin is removed. Clean up event listeners, timers, etc. ```typescript override detach(): void { this.state.clear(); clearInterval(this.timer); super.detach(); // Always call super last! } ``` ### `processColumns(columns)` Transform column definitions before rendering. Return the modified array. ```typescript override processColumns(columns: ColumnConfig[]): ColumnConfig[] { return [ ...columns, { field: '__rowNumber', header: '#', width: 50, renderer: (ctx) => String(ctx.rowIndex + 1), }, ]; } ``` ### `processRows(rows)` Transform row data before rendering. Return the modified array. ```typescript override processRows(rows: T[]): T[] { return rows.filter(row => row.active); } ``` ### `afterRender()` Called after each render cycle. Use for grid-wide DOM manipulation. ```typescript override afterRender(): void { const gridEl = this.gridElement; const rows = gridEl.querySelectorAll('.data-grid-row'); rows.forEach((row, i) => { if (i % 2 === 0) row.classList.add('even-row'); }); } ``` ### `afterCellRender(context)` Called after each cell is rendered. More efficient than `afterRender` for per-cell modifications because you receive the cell context directly—no DOM queries needed. ```typescript import type { AfterCellRenderContext } from '@toolbox-web/grid'; override afterCellRender(context: AfterCellRenderContext): void { const { row, rowIndex, colIndex, value, cellElement, column } = context; // Add selection class without DOM queries if (this.isSelected(rowIndex, colIndex)) { cellElement.classList.add('selected'); } // Add validation error styling if (this.hasError(row, column.field)) { cellElement.classList.add('has-error'); } } ``` :::note[Performance] `afterCellRender` is called for every visible cell during render and scroll. Keep implementation fast. ::: ### `afterRowRender(context)` Called after a row is fully rendered (all cells complete). Use for row-level decorations, styling, or ARIA attributes. ```typescript import type { AfterRowRenderContext } from '@toolbox-web/grid'; override afterRowRender(context: AfterRowRenderContext): void { const { row, rowIndex, rowElement } = context; // Add row selection class without DOM queries if (this.isRowSelected(rowIndex)) { rowElement.classList.add('selected', 'row-focus'); } // Add validation error styling if (this.rowHasErrors(row)) { rowElement.classList.add('has-errors'); } } ``` :::note[Performance] `afterRowRender` is called for every visible row during render and scroll. Keep implementation fast. ::: ## Built-in Plugin Helpers [`BaseGridPlugin`](https://toolboxjs.com/grid/api/plugin-development/classes/basegridplugin.md) provides protected helpers: | Helper | Description | |--------|-------------| | `this.grid` | Typed `GridElement` (extends `GridElementRef`) | | `this.gridElement` | Grid as `HTMLElement` for DOM queries | | `this.columns` | Current column configurations | | `this.visibleColumns` | Only visible columns | | `this.rows` | Processed rows (after filtering, grouping) | | `this.sourceRows` | Original unfiltered rows | | `this.disconnectSignal` | `AbortSignal` for auto-cleanup | | `this.isAnimationEnabled` | Whether animations are enabled | | `this.animationDuration` | Animation duration in ms | | `this.gridIcons` | Merged icon configuration | | `this.getPluginByName(name)` | Get another plugin by name (preferred) | | `this.getPlugin(PluginClass)` | Get another plugin by class (alternative) | | `this.emit(eventName, detail)` | Dispatch a DOM `CustomEvent` from the grid (external consumers) | | `this.broadcast(eventType, detail)` | Emit on the internal event bus + DOM (other plugins + external consumers) | | `this.on(eventType, callback)` | Subscribe to event-bus events (auto-cleaned on detach) | | `this.requestRender()` | Request full re-render | | `this.requestAfterRender()` | Request lightweight style update | | `this.requestVirtualRefresh()` | Re-render visible rows without rebuilding the row model | | `this.setIcon(el, iconKey)` | Set icon on element using CSS-first hybrid approach | ## Event Hooks ### `onCellClick(event)` Handle cell click events. Return `true` to prevent default behavior. ```typescript import type { CellClickEvent } from '@toolbox-web/grid'; override onCellClick(event: CellClickEvent): boolean | void { const { row, field, value, cellEl } = event; if (field === 'delete') { this.handleDelete(row); return true; // Prevent default click handling } return false; // Allow normal processing } ``` ### `onCellMouseDown(event)` Handle mousedown for drag operations or selection. ```typescript import type { CellMouseEvent } from '@toolbox-web/grid'; override onCellMouseDown(event: CellMouseEvent): boolean | void { const { row, field } = event; if (field === 'drag-handle') { this.startDrag(row); return true; } return false; } ``` ### `onKeyDown(event)` Handle keyboard events. Return `true` to prevent default. ```typescript override onKeyDown(event: KeyboardEvent): boolean | void { if (event.ctrlKey && event.key === 'd') { this.duplicateSelectedRow(); return true; } return false; } ``` ### `onScroll(event)` Respond to scroll events (use sparingly — can impact performance). ```typescript import type { ScrollEvent } from '@toolbox-web/grid'; override onScroll(event: ScrollEvent): void { this.updateStickyElements(event.scrollTop); } ``` ### `renderRow(row, rowEl, rowIndex)` Custom row rendering. Return `true` to skip default rendering. ```typescript override renderRow(row: T, rowEl: HTMLElement, rowIndex: number): boolean | void { if (row.type === 'section-header') { rowEl.innerHTML = `<div class="section-header">${row.title}</div>`; return true; } return false; } ``` ## Injecting Styles Plugins can inject CSS via the `styles` property (uses `adoptedStyleSheets`): #### External CSS File ```typescript // Import CSS as string (Vite) import styles from './my-plugin.css?inline'; export class MyPlugin extends BaseGridPlugin { override readonly styles = styles; } ``` ```css /* my-plugin.css */ .my-custom-class { background: #f0f0f0; border-left: 3px solid #1976d2; } ``` #### Inline Styles ```typescript export class MyPlugin extends BaseGridPlugin { override readonly styles = ` .my-custom-class { background: #f0f0f0; border-left: 3px solid #1976d2; } .my-custom-class:hover { background: #e0e0e0; } `; } ``` :::caution Do not append `<style>` elements as **children of `<tbw-grid>`** — the grid calls `replaceChildren()` during renders, which removes child nodes. For plugin styles, use the `styles` property (shown above) or `this.gridElement.registerStyles(id, css)`. Note: external CSS (stylesheets, `<style>` in `<head>`) is unaffected and works normally since the grid uses light DOM. ::: ## Accessing Grid State ```typescript override afterRender(): void { // Access current rows (after processing) const rows = this.grid.rows; // Access effective config const config = this.grid.gridConfig; // Access sort state const sortState = this.grid.sortState; // Access changed rows (editing) const changes = this.grid.changedRows; // Request a full re-render (internal API for plugins) this.grid.requestRender(); // Request only afterRender hooks (lightweight update) this.grid.requestAfterRender(); } ``` ## Plugin Manifest The **Plugin Manifest** is a static property that declares metadata about your plugin's capabilities and requirements. The grid uses this metadata for: - **Validation**: Detect missing plugins when their properties are used - **Configuration rules**: Warn or error on invalid config combinations - **Query routing**: Efficiently route queries only to plugins that handle them - **Event discovery**: Document events that other plugins can subscribe to - **Incompatibility detection**: Warn when conflicting plugins are loaded together ### Manifest Structure ```typescript import { BaseGridPlugin, type PluginManifest } from '@toolbox-web/grid'; interface MyPluginConfig { optionA?: boolean; optionB?: boolean; } export class MyPlugin extends BaseGridPlugin<MyPluginConfig> { static override readonly manifest: PluginManifest<MyPluginConfig> = { // Properties this plugin owns (for validation) ownedProperties: [ { property: 'myColumnProp', level: 'column', description: 'the "myColumnProp" column property', }, { property: 'myGridOption', level: 'config', description: 'the "myGridOption" grid config option', }, ], // Configuration validation rules configRules: [ { id: 'my-plugin/invalid-combo', severity: 'warn', message: 'optionA and optionB cannot both be true', check: (config) => config.optionA === true && config.optionB === true, }, ], // Queries this plugin handles queries: [ { type: 'getMyState', description: 'Get the current plugin state' }, ], // Events this plugin emits events: [ { type: 'my-state-change', description: 'Emitted when state changes' }, ], // Plugins that conflict with this one incompatibleWith: [ { name: 'conflictingPlugin', reason: 'Both plugins modify the same DOM elements' }, ], // Hook execution priority (lower = earlier, default 0) hookPriority: { processRows: 100, // Run after most plugins afterRender: -50, // Run before most plugins }, }; readonly name = 'myPlugin'; } ``` ### Owned Properties Declare properties your plugin adds to [`ColumnConfig`](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md) or [`GridConfig`](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md). If a user configures these properties without loading your plugin, the grid throws a helpful error: ```typescript ownedProperties: [ { property: 'editable', level: 'column', description: 'the "editable" column property', importHint: "import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';", }, ], ``` Error shown to user: ``` [tbw-grid] Configuration error: Column(s) [name, email] use the "editable" column property, but the required plugin is not loaded. → Add the plugin to your gridConfig.plugins array: import { EditingPlugin } from '@toolbox-web/grid/plugins/editing'; plugins: [new EditingPlugin(), ...] ``` ### Configuration Rules ```typescript configRules: [ { id: 'selection/range-dblclick', severity: 'warn', message: '"triggerOn: dblclick" has no effect when mode is "range".', check: (config) => config.mode === 'range' && config.triggerOn === 'dblclick', }, ], ``` - `severity: 'error'` — Throws an exception (always) - `severity: 'warn'` — Logs to console (development only) ### Hook Priority By default, hooks execute in plugin array order. Use `hookPriority` to override the execution order for specific hooks without changing the plugin array: ```typescript static override readonly manifest: PluginManifest = { hookPriority: { processRows: 100, // Higher value → runs later afterRender: -50, // Negative value → runs earlier }, }; ``` - Default priority is **0** for all hooks - **Lower values** execute first; **higher values** execute later - Plugins with equal priority preserve their original array order - Priority is configured **per hook** — a plugin can run early for one hook and late for another Available hooks: `processColumns`, `processRows`, `afterRender`, `afterCellRender`, `afterRowRender`, `onHeaderClick`, `onRowClick`, `onCellClick`, `onCellMouseDown`, `onCellMouseMove`, `onCellMouseUp`, `onKeyDown`, `onScroll`, `onScrollRender`. That list is exported as the [`HookName`](https://toolboxjs.com/grid/api/plugin-development/types/hookname.md) union, so you can type your own hook-aware code against it instead of re-declaring the strings: ```typescript import type { HookName } from '@toolbox-web/grid'; // `hookPriority` is `Partial<Record<HookName, number>>` — a typo is a compile error. const priorities: Partial<Record<HookName, number>> = { processRows: 100 }; ``` The payload types for the pointer/keyboard hooks are public too — [`CellClickEvent`](https://toolboxjs.com/grid/api/core/interfaces/cellclickevent.md), [`RowClickEvent`](https://toolboxjs.com/grid/api/core/interfaces/rowclickevent.md), [`HeaderClickEvent`](https://toolboxjs.com/grid/api/core/interfaces/headerclickevent.md), [`ScrollEvent`](https://toolboxjs.com/grid/api/core/interfaces/scrollevent.md), and [`CellCoords`](https://toolboxjs.com/grid/api/core/interfaces/cellcoords.md) — so a plugin can annotate its hook signatures explicitly rather than relying on inference. ### Plugin Dependencies Declare required plugins via a static `dependencies` property: ```typescript export class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> { static override readonly dependencies: PluginDependency[] = [ { name: 'editing', required: true, reason: 'Tracks cell edit history' }, { name: 'selection', required: false, reason: 'Enables selection-based undo' }, ]; readonly name = 'undoRedo'; } ``` :::danger When using the manual `plugins` array, dependencies must be loaded *before* the dependent plugin. Wrong order **throws a runtime error**: ```typescript // ✅ Correct order plugins: [new EditingPlugin(), new UndoRedoPlugin()] // ❌ Wrong order — throws error plugins: [new UndoRedoPlugin(), new EditingPlugin()] ``` ::: :::tip The **features API** handles dependency ordering automatically. When you use `features: { ... }`, the grid reorders plugins based on declared dependencies — no manual ordering required, and import order doesn't matter (safe from Prettier's alphabetical sorting). ```typescript // Order doesn't matter — the grid resolves dependencies for you import '@toolbox-web/grid/features/undo-redo'; import '@toolbox-web/grid/features/editing'; grid.gridConfig = { features: { editing: true, undoRedo: true }, }; ``` ::: #### Conditional dependencies and severity A dependency can be made **config-conditional** with a `when` predicate, and its missing-dependency behavior tuned with `severity`: ```typescript export class PivotPlugin extends BaseGridPlugin<PivotConfig> { static override readonly dependencies: PluginDependency[] = [ { name: 'shell', // Only needed when the tool panel is enabled required: false, when: (cfg) => (cfg as PivotConfig).showToolPanel === true, severity: 'warn', reason: 'PivotPlugin needs a tool-panel host when showToolPanel is enabled', }, ]; readonly name = 'pivot'; } ``` - **`when(pluginConfig)`** — receives the depending plugin's resolved config (defaults merged with user config) and is evaluated **before** the plugin attaches. Return `false` to skip the dependency entirely. When omitted, the dependency always applies. - **`severity`** — overrides the default derived from `required`: - `'error'` — throws and halts grid setup (the default when `required` is not `false`). - `'warn'` — logs a `console.warn` and continues (development only). - `'info'` — logs a verbose `console.debug` and continues (development only). When `severity` is omitted, a missing **hard** dependency (`required !== false`) throws and a missing **soft** dependency (`required: false`) stays silent — preserving backward-compatible behavior. Opt into a message by setting `severity` explicitly. ## Plugin Communication ### Event Bus (Plugin-to-Plugin) Plugins can emit and subscribe to events using the built-in Event Bus. Events are automatically cleaned up when a plugin is detached. ```typescript import { BaseGridPlugin, type PluginManifest } from '@toolbox-web/grid'; // Plugin A: Emit events export class FilterPlugin extends BaseGridPlugin<FilterConfig> { readonly name = 'filtering'; // Declare events in manifest for discoverability static override readonly manifest: PluginManifest = { events: [ { type: 'filter-change', description: 'Emitted when filters change' }, ], }; applyFilter(criteria: FilterCriteria): void { // ... filter logic // Broadcast to both DOM consumers and other plugins this.broadcast('filter-change', { criteria, rowCount: this.rows.length }); } } // Plugin B: Subscribe to events export class SelectionPlugin extends BaseGridPlugin<SelectionConfig> { readonly name = 'selection'; override attach(grid: GridElement): void { super.attach(grid); // Subscribe — auto-cleaned on detach this.on('filter-change', (detail) => { console.log('Filter changed, clearing selection'); this.clearSelection(); }); } } ``` ### Query System (Synchronous State Retrieval) Plugins can expose queryable state that other plugins can retrieve synchronously. ```typescript import { BaseGridPlugin, type PluginManifest, type PluginQuery } from '@toolbox-web/grid'; // Plugin A: Handle queries export class SelectionPlugin extends BaseGridPlugin<SelectionConfig> { readonly name = 'selection'; // Declare queries in manifest for routing static override readonly manifest: PluginManifest = { queries: [ { type: 'getSelection', description: 'Get current selection state' }, ], }; override handleQuery(query: PluginQuery): unknown { if (query.type === 'getSelection') { return this.getSelection(); } return undefined; } } // Plugin B: Query other plugins export class ClipboardPlugin extends BaseGridPlugin<ClipboardConfig> { readonly name = 'clipboard'; copy(): void { const responses = this.grid.query<SelectionResult>('getSelection', undefined); const selection = responses[0]; if (selection?.ranges.length > 0) { // Copy selected cells } } } ``` ### DOM Events (External Consumers) For events that external code should listen to, use `emit()`: ```typescript this.emit('copy', { text: copiedText, rowCount: 5 }); // External code: grid.on('copy', (detail) => console.log(detail)); ``` ## TypeScript Generics For type-safe row access, use generics: ```typescript export class TypedPlugin<T extends { id: number }> extends BaseGridPlugin<{ idField: keyof T }> { override processRows(rows: T[]): T[] { const idField = this.config.idField; return rows.filter(row => row[idField] != null); } } // Usage new TypedPlugin<Employee>({ idField: 'employeeId' }); ``` ## Complete Example: Row Numbering Plugin This example adds a synthetic column that doesn't map to any field in the data. The `__rowNumber` field doesn't exist on row objects—the renderer uses `ctx.rowIndex` instead of `ctx.value`. This pattern is safe because: - `processColumns` only modifies column definitions, not row data - Renderers produce DOM output—they don't write back to the source data - The `rows` array remains unchanged ```typescript import { BaseGridPlugin, type ColumnConfig } from '@toolbox-web/grid'; interface RowNumberConfig { header?: string; width?: number; startFrom?: number; } export class RowNumberPlugin extends BaseGridPlugin<RowNumberConfig> { readonly name = 'rowNumber'; override readonly styles = ` .row-number-cell { color: #888; font-size: 0.85em; text-align: center; } `; override processColumns(columns: ColumnConfig[]): ColumnConfig[] { const { header = '#', width = 50, startFrom = 1 } = this.config; return [ { field: '__rowNumber', header, width, minWidth: 40, maxWidth: 80, resizable: false, sortable: false, renderer: (ctx) => { const span = document.createElement('span'); span.className = 'row-number-cell'; span.textContent = String(ctx.rowIndex + startFrom); return span; }, }, ...columns, ]; } } ``` ## Best Practices 1. **Always call `super`** — In `attach()` call it first, in `detach()` call it last 2. **Use unique names** — Plugin `name` should be unique across all plugins 3. **Clean up resources** — Remove event listeners and timers in `detach()` 4. **Return modified data** — `processColumns` and `processRows` must return arrays 5. **Use `ctx.signal`** — For automatic cleanup of event listeners in renderers 6. **Avoid heavy operations** — Keep hooks fast, especially `onScroll` and `afterRender` 7. **Document your config** — Use JSDoc comments for IDE support 8. **Version your plugins** — Increment version when making breaking changes ## Testing Plugins ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { RowNumberPlugin } from './row-number-plugin'; describe('RowNumberPlugin', () => { let plugin: RowNumberPlugin; beforeEach(() => { plugin = new RowNumberPlugin({ startFrom: 1 }); }); it('should add row number column', () => { const columns = plugin.processColumns([ { field: 'name', header: 'Name' }, ]); expect(columns).toHaveLength(2); expect(columns[0].field).toBe('__rowNumber'); }); }); ``` ## Registering as a Feature Plugins can be promoted to **features** so consumers can enable them via the declarative `features` config instead of manually instantiating classes. This also enables shorthand syntax and tree-shaking. ### 1. Create the Feature Module Create a feature file that registers your plugin with a factory function: ```typescript // my-feature.ts (e.g., libs/grid/src/lib/features/row-numbers.ts) import { RowNumberPlugin, type RowNumberConfig } from '../plugins/row-numbers'; import { registerFeature } from './registry'; // Augment the FeatureConfig interface so TypeScript knows about your feature declare module '../core/types' { interface FeatureConfig { /** Enable row numbering. Shorthand: `true` or a start number. */ rowNumbers?: boolean | number | RowNumberConfig; } } // Register the factory — convert shorthands to full plugin config registerFeature('rowNumbers', (config) => { if (config === true) { return new RowNumberPlugin(); // defaults } if (typeof config === 'number') { return new RowNumberPlugin({ startFrom: config }); // shorthand } return new RowNumberPlugin(config as RowNumberConfig); // full config }); ``` ### 2. The Pattern Every feature module follows the same three steps: 1. **Import** the plugin class and its config type 2. **Augment** `FeatureConfig` via `declare module` — this adds your shorthand types to the `features` object 3. **Call** `registerFeature(name, factory)` — the factory receives the raw config value and returns a plugin instance The factory function is responsible for converting shorthand values (`true`, `'row'`, a number, etc.) into the full plugin constructor config. ### 3. How It Works at Runtime Feature modules are **side-effect imports** — importing the file automatically registers the feature: ```typescript // Consumer code import '@toolbox-web/grid/features/row-numbers'; // ← registers the feature grid.gridConfig = { features: { rowNumbers: 5, // shorthand: start from 5 }, }; ``` If no one imports the feature module, it doesn't exist in the bundle — this is how tree-shaking works. ### 4. Common Shorthand Patterns | Shorthand Type | Example | Usage | |---------------|---------|-------| | `boolean` | `filtering: true` | Enable with defaults | | String literal | `selection: 'row'` | Pick a mode | | `number` | `rowNumbers: 5` | Single numeric option | | Full config | `editing: { editOn: 'click', ... }` | Full control | ### 5. Declaring Dependencies If your feature depends on other features, declare it in the `PLUGIN_DEPENDENCIES` map in `registry.ts`: ```typescript // In registry.ts const PLUGIN_DEPENDENCIES: Record<string, string[]> = { clipboard: ['selection'], // clipboard needs selection rowNumbers: [], // no dependencies }; ``` The grid reorders plugins automatically based on declared dependencies and warns if a dependency is missing. ### 6. Framework Adapter Integration For the feature to work with framework adapters (React, Vue, Angular), each adapter needs to re-export or reference the feature. The typical pattern: ```typescript // libs/grid-react/src/features/row-numbers.ts import '@toolbox-web/grid/features/row-numbers'; ``` This ensures the side-effect import is available when consumers import from the adapter package: ```tsx import '@toolbox-web/grid-react/features/row-numbers'; <DataGrid rows={rows} gridConfig={{ features: { rowNumbers: true } }} /> ``` --- # Framework Adapters > How React, Angular, and Vue adapters let @toolbox-web/grid render framework-native components as cells, editors, and tool panels. `@toolbox-web/grid` is a framework-agnostic web component, but most applications want their cell renderers, editors, and tool panels written in their host framework — JSX, Angular templates, Vue SFCs. **Framework adapters** are the bridge: each adapter intercepts grid rendering and mounts the appropriate framework primitive at the right DOM node. Three official adapters ship today: - [Angular](https://toolboxjs.com/grid/angular/getting-started.md): @toolbox-web/grid-angular - [React](https://toolboxjs.com/grid/react/getting-started.md): @toolbox-web/grid-react - [Vue](https://toolboxjs.com/grid/vue/getting-started.md): @toolbox-web/grid-vue You can also write your own adapter for any framework (Svelte, Solid, Lit, plain custom elements) — the contract is small and stable. - [Architecture](https://toolboxjs.com/grid/framework-adapters/architecture.md): How adapters integrate into the grid lifecycle, the FrameworkAdapter interface, registration, and invocation points. - [API Reference](https://toolboxjs.com/grid/api/framework-adapters.md): Typed reference for FrameworkAdapter, registration helpers, and the cell-renderer / editor contracts. --- # Framework Adapter Architecture > How the @toolbox-web/grid framework-adapter system intercepts grid rendering to mount React, Angular, and Vue components in cells, editors, and tool panels. Framework adapters let React, Angular, and Vue intercept grid rendering to support JSX components, Angular templates, and Vue slots as cell renderers and editors. For an introduction and links to the official adapters, see [Framework Adapters](https://toolboxjs.com/grid/framework-adapters.md). For grid-core internals see [Architecture](https://toolboxjs.com/grid/architecture.md). For typed signatures see the [`FrameworkAdapter` API reference](https://toolboxjs.com/grid/api/framework-adapters/interfaces/frameworkadapter.md). ## Architecture ```mermaid flowchart TB subgraph adapters["REGISTERED ADAPTERS"] A["ReactGridAdapter"] B["AngularGridAdapter"] C["VueGridAdapter"] end D["DataGridElement.registerAdapter()"] A --> D B --> D C --> D D --> E["Static adapter array<br/><i>(shared across all grids)</i>"] subgraph rendering["RENDERING PIPELINE"] F["Parse <tbw-grid-column>"] G["Cell render cycle"] H["Cell cleanup"] end E --> F E --> G E --> H ``` ## FrameworkAdapter Interface Adapters implement these methods: | Method | Required | Purpose | |--------|----------|---------| | `canHandle(element)` | Yes | Check if this adapter can process the element | | `createRenderer(element)` | Yes | Return a cell renderer function, or `undefined` to pass | | `createEditor(element)` | Yes | Return a cell editor function, or `undefined` to pass | | `processConfig(config)` | No | Transform framework-specific config (e.g., component classes → render functions) | | `getTypeDefault(type)` | No | Provide app-wide type defaults from framework's DI/registry | | `releaseCell(cellEl)` | No | Cleanup when cell DOM is recycled (unmount React roots, destroy Angular views) | | `createToolPanelRenderer(element)` | No | Support framework components in tool panel sidebar | ## Registration Adapters register statically on `DataGridElement` — typically at module load time: ```typescript // React: registers immediately on import const globalAdapter = new GridAdapter(); DataGridElement.registerAdapter(globalAdapter); // Angular: registers via Grid directive DataGridElement.registerAdapter(this.adapter); // Vue: registers in DataGrid component onMounted() ``` This is why importing `@toolbox-web/grid-react` (or any adapter package) auto-registers the adapter — no separate setup needed. ## Adapter Invocation Points Adapters are called at four points in the grid lifecycle: **1. Light DOM Column Parsing** — When `<tbw-grid-column>` elements are parsed, each adapter is tried until one handles the renderer/editor: ```typescript const viewAdapter = adapters.find((a) => a.canHandle(viewTarget)); if (viewAdapter) { config.viewRenderer = viewAdapter.createRenderer(viewTarget); } ``` **2. Config Processing** — When `gridConfig` is set, the adapter can transform component classes to render functions: ```typescript set gridConfig(value) { if (value && this.__frameworkAdapter?.processConfig) { value = this.__frameworkAdapter.processConfig(value); } } ``` **3. Cell Rendering** — Type defaults are resolved through the adapter: ```typescript const appDefault = adapter.getTypeDefault(col.type); if (appDefault?.renderer) return appDefault.renderer; ``` **4. Cell Cleanup** — When virtualization recycles a row, adapters unmount framework components: ```typescript grid.__frameworkAdapter?.releaseCell?.(cell); // React: unmounts React root // Angular: destroys component view // Vue: unmounts app instance ``` --- # Base Classes for Editors & Filter Panels > BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, and BaseFilterPanel — reusable base classes for custom Angular editors and filter panels. The `@toolbox-web/grid-angular` package provides base classes that eliminate boilerplate when building custom editors and filter panels. Each class handles common infrastructure — lifecycle, cleanup, positioning, form integration — so you can focus on your component's UI and logic. ## Class Hierarchy ``` BaseGridEditor ← common inputs/outputs, validation helpers ├── BaseGridEditorCVA ← + ControlValueAccessor (dual grid/form use) └── BaseOverlayEditor ← + floating overlay panel infrastructure ``` | Class | Purpose | Extend when… | |-------|---------|--------------| | `BaseGridEditor` | Inline cell editors | You need a simple input that renders inside the cell | | `BaseGridEditorCVA` | Dual grid + form editors | The same component is used inside `<tbw-grid>` **and** standalone `<form>` | | `BaseOverlayEditor` | Overlay/popup editors | You need a floating panel (date picker, autocomplete, dropdown) | | `BaseFilterPanel` | Column filter panels | You need a custom filter UI for the FilteringPlugin | --- ## BaseGridEditor The foundation class for all Angular cell editors. Provides common inputs, outputs, validation state, and automatic cleanup. > **Full API:** [`BaseGridEditor`](https://toolboxjs.com/grid/angular/api/utilities/basegrideditor.md) ### API The class exposes four inputs (`value`, `row`, `column`, `control`), two outputs (`commit`, `cancel`), and a set of signals derived from the bound `FormControl` — `currentValue()`, `isInvalid()`, `isDirty()`, `isTouched()`, `hasErrors()`, `firstErrorMessage()` and `allErrorMessages()`. Every signal that mirrors control state returns `false` (or an empty value) when no `control` is bound, so an editor works unchanged outside a `FormArray`. Commit through `commitValue(v)` and abort through `cancelEdit()` — both emit the Angular output *and* the DOM `CustomEvent` the grid listens for. Override `getErrorMessage(key, value?)` to customise validation text, and the `onBeforeEditClose()` / `onEditClose()` / `onExternalValueChange(v)` hooks to flush pending values, tear down overlays, or react to cascading edits. See [`BaseGridEditor`](https://toolboxjs.com/grid/angular/api/utilities/basegrideditor.md) for every member with its exact signature. ### Example ```typescript import { Component } from '@angular/core'; import { BaseGridEditor } from '@toolbox-web/grid-angular/features/editing'; @Component({ selector: 'app-text-editor', template: ` <input [value]="currentValue()" [class.is-invalid]="isInvalid()" (input)="commitValue($event.target.value)" (keydown.escape)="cancelEdit()" /> @if (hasErrors()) { <div class="error">{{ firstErrorMessage() }}</div> } ` }) export class TextEditorComponent extends BaseGridEditor<MyRow, string> { protected override getErrorMessage(errorKey: string): string { if (errorKey === 'required') return 'This field is required'; return super.getErrorMessage(errorKey); } } ``` ### Validation State & Reactivity The validation signals — `isInvalid`, `isDirty`, `isTouched`, `hasErrors`, `firstErrorMessage`, `allErrorMessages` — are **derived from the `control` input**. They only carry meaning when the grid is bound to a `FormArray` and an `AbstractControl` has been passed in via `[control]="control"`. Without a control, they default to `false` / `''` / `[]`. Because they are Angular `computed()` signals: - They **don't run on a schedule** — they re-evaluate lazily, only when read. - They re-fire automatically whenever `control.errors`, `control.invalid`, `control.dirty`, or `control.touched` changes — which Angular triggers on every value update, blur, or programmatic call to `markAsTouched()` / `updateValueAndValidity()`. - If your template never reads them, they never compute. **Error pipeline** — `firstErrorMessage()` and `allErrorMessages()` walk the `control.errors` map and pipe each `(key, value)` pair through your `getErrorMessage()` override. The first-vs-all distinction: - **`firstErrorMessage()`** — the most common case; show one inline message (`{{ firstErrorMessage() }}` under the input). - **`allErrorMessages()`** — when a single field can have several simultaneous errors you want to surface together (password complexity, multi-rule validators) rendered as a list. --- ## BaseGridEditorCVA Combines `BaseGridEditor` with Angular's `ControlValueAccessor` interface. Use this when a single component needs to work both as a grid cell editor **and** as a standalone form control. > **Full API:** [`BaseGridEditorCVA`](https://toolboxjs.com/grid/angular/api/utilities/basegrideditorcva.md) ### Additional API (on top of BaseGridEditor) | Member | Type | Description | |--------|------|-------------| | `cvaValue` | `Signal<TValue \| null>` | Value written by the form control | | `disabledState` | `Signal<boolean>` | Tracks `setDisabledState()` calls | | `displayValue` | `Computed<TValue \| null>` | Prefers the last `commitBoth()` value, then the grid value, then the CVA value | | `commitBoth(v)` | Method | Commits via both CVA `onChange` **and** grid `commitValue` | | `writeValue` / `registerOn*` / `setDisabledState` | CVA methods | Full `ControlValueAccessor` implementation | ### Example ```typescript import { Component, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { BaseGridEditorCVA } from '@toolbox-web/grid-angular/features/editing'; @Component({ selector: 'app-date-picker', providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DatePickerComponent), multi: true, }], template: ` <input type="date" [value]="displayValue()" [disabled]="disabledState()" (change)="commitBoth($event.target.value)" (keydown.escape)="cancelEdit()" /> ` }) export class DatePickerComponent extends BaseGridEditorCVA<MyRow, string> {} ``` :::note Subclasses must still provide `NG_VALUE_ACCESSOR` themselves because `forwardRef(() => ConcreteClass)` must reference the concrete component — this is an Angular limitation. ::: ### Using in both contexts ```typescript // Inside a grid <tbw-grid-column field="startDate" editable> <app-date-picker *tbwEditor="let value" [value]="value" /> </tbw-grid-column> // Inside a standalone form <app-date-picker formControlName="startDate" /> ``` --- ## BaseOverlayEditor Base class for editors that display a **floating overlay panel** (date pickers, autocomplete dropdowns, color pickers, etc.). Handles all the positioning, focus gating, click-outside detection, and cleanup infrastructure. > **Full API:** [`BaseOverlayEditor`](https://toolboxjs.com/grid/angular/api/utilities/baseoverlayeditor.md) · [`OverlayPosition`](https://toolboxjs.com/grid/angular/api/types/overlayposition.md) ### Features - **CSS Anchor Positioning** with JS `getBoundingClientRect()` fallback for Firefox/Safari - **Focus gating** — in row editing mode, only the focused cell's overlay is shown - **Click-outside detection** — configurable via `onOverlayOutsideClick()` - **Keyboard routing** — Enter/Space/ArrowDown/F2 open, Escape closes - **Automatic teardown** — panel removed from `<body>` + all listeners cleaned up on destroy ### API | Member | Type | Description | |--------|------|-------------| | `overlayPosition` | `OverlayPosition` | Position relative to cell: `'below'` (default), `'above'`, `'below-right'`, `'over-top-left'`, `'over-bottom-left'` | | `initOverlay(panel)` | Method | Initialize with the panel element. Call in an `effect()` or `afterNextRender()`. | | `showOverlay()` | Method | Show the overlay panel | | `hideOverlay(suppressTab?)` | Method | Hide the overlay panel | | `reopenOverlay()` | Method | Close and re-open (repositions after content change) | | `teardownOverlay()` | Method | Remove panel from DOM + cleanup (auto-called on destroy) | | `onInlineKeydown(e)` | Method | Keydown handler for the inline input | | `onInlineClick()` | Method | Click handler for the inline input (toggle overlay) | | `handleEscape(e)` | Method | Close overlay or cancel edit | | `advanceGridFocus(backward?)` | Method | Dispatch synthetic Tab to move to next cell | | `getInlineInput()` | **Abstract** | Return the inline `<input>` element (for focus return) | | `onOverlayOutsideClick()` | **Abstract** | Called on click outside — typically call `hideOverlay()` | | `onOverlayOpened()` | Hook | Called after overlay opens (focus inner element, etc.) | ### Example ```typescript import { Component, viewChild, ElementRef, effect } from '@angular/core'; import { BaseOverlayEditor } from '@toolbox-web/grid-angular/features/editing'; @Component({ selector: 'app-date-editor', template: ` <input #inlineInput readonly [value]="currentValue()" (click)="onInlineClick()" (keydown)="onInlineKeydown($event)" /> <div #panel class="tbw-overlay-panel" style="width: 280px; padding: 12px;"> <input type="date" [value]="currentValue()" (change)="selectAndClose($event.target.value)" /> <div class="actions"> <button (click)="hideOverlay()">Cancel</button> </div> </div> ` }) export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> { panelRef = viewChild.required<ElementRef<HTMLElement>>('panel'); inputRef = viewChild.required<ElementRef<HTMLInputElement>>('inlineInput'); protected override overlayPosition = 'below' as const; constructor() { super(); effect(() => { const panel = this.panelRef().nativeElement; this.initOverlay(panel); if (this.isCellFocused()) this.showOverlay(); }); } protected getInlineInput(): HTMLInputElement | null { return this.inputRef()?.nativeElement ?? null; } protected onOverlayOutsideClick(): void { this.hideOverlay(); } selectAndClose(date: string): void { this.commitValue(date); this.hideOverlay(); } } ``` ### Overlay Position Options | Value | Description | |-------|-------------| | `'below'` | Panel below the cell, left-aligned (default). Flips above if viewport overflows. | | `'above'` | Panel above the cell, left-aligned. Flips below if off-screen. | | `'below-right'` | Panel below the cell, right-aligned. Flips above if viewport overflows. | | `'over-top-left'` | Panel top-left aligns with cell top-left (opens downward). No flip. | | `'over-bottom-left'` | Panel bottom-left aligns with cell bottom-left (opens upward). No flip. | ### CSS Customization The overlay panel uses CSS custom properties from the grid theme: | Variable | Default | Description | |----------|---------|-------------| | `--tbw-overlay-bg` | `#fff` | Panel background | | `--tbw-overlay-border` | `#ccc` | Panel border color | | `--tbw-overlay-radius` | `4px` | Panel border radius | | `--tbw-overlay-shadow` | `0 4px 12px rgba(0,0,0,0.15)` | Panel box shadow | --- ## BaseFilterPanel Base class for custom column filter panels used with the `FilteringPlugin`. Provides the `params` input and lifecycle helpers so you only need to implement your filter logic. > **Full API:** [`BaseFilterPanel`](https://toolboxjs.com/grid/angular/api/utilities/basefilterpanel.md) · [`FilterPanelParams`](https://toolboxjs.com/grid/plugins/filtering/interfaces/filterpanelparams.md) ### API | Member | Type | Description | |--------|------|-------------| | `params` | `input.required<FilterPanelParams>()` | Injected by the grid's filtering infrastructure | | `applyFilter()` | **Abstract** | Implement your filter logic here | | `applyAndClose()` | Method | Calls `applyFilter()` then `closePanel()` | | `clearAndClose()` | Method | Calls `clearFilter()` then `closePanel()` | ### FilterPanelParams The `params` input carries the field being filtered, its column config, the available values (`uniqueValues`, `excludedValues`, `searchText`), the active `currentFilter` to restore UI state from, and the callbacks that write back: `applySetFilter(excludedValues, valueTo?)` — note it takes an **array**, not a `Set` — `applyTextFilter(operator, value, valueTo?)`, `clearFilter()` and `closePanel()`. See [`FilterPanelParams`](https://toolboxjs.com/grid/plugins/filtering/interfaces/filterpanelparams.md) for the full typed contract. ### Example: Text Filter ```typescript import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core'; import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering'; @Component({ selector: 'app-text-filter', template: ` <div class="filter-panel"> <input #input placeholder="Search..." (keydown.enter)="applyAndClose()" /> <div class="actions"> <button (click)="applyAndClose()">Apply</button> <button (click)="clearAndClose()">Clear</button> </div> </div> ` }) export class TextFilterComponent extends BaseFilterPanel { input = viewChild.required<ElementRef<HTMLInputElement>>('input'); constructor() { super(); afterNextRender(() => { this.input().nativeElement.focus(); }); } applyFilter(): void { this.params().applyTextFilter('contains', this.input().nativeElement.value); } } ``` ### Example: Set Filter (Checkbox List) ```typescript import { Component, signal } from '@angular/core'; import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering'; @Component({ selector: 'app-set-filter', template: ` <div class="filter-panel"> @for (val of params().uniqueValues; track val) { <label> <input type="checkbox" [checked]="!excluded().has(val)" (change)="toggle(val)" /> {{ val }} </label> } <div class="actions"> <button (click)="applyAndClose()">Apply</button> <button (click)="clearAndClose()">Clear</button> </div> </div> ` }) export class SetFilterComponent extends BaseFilterPanel { excluded = signal(new Set<unknown>()); toggle(value: unknown): void { this.excluded.update(set => { const next = new Set(set); next.has(value) ? next.delete(value) : next.add(value); return next; }); } applyFilter(): void { // applySetFilter takes an array, not a Set this.params().applySetFilter([...this.excluded()]); } } ``` ### Usage in Column Config ```typescript import type { GridConfig } from '@toolbox-web/grid-angular'; import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering'; import { TextFilterComponent } from './text-filter.component'; import { SetFilterComponent } from './set-filter.component'; const gridConfig: GridConfig = { columns: [ { field: 'name', filterable: true, filterPanel: TextFilterComponent }, { field: 'status', filterable: true, filterPanel: SetFilterComponent }, ], features: { filtering: true }, }; ``` --- ## When to Use Which Class | Scenario | Base Class | |----------|-----------| | Simple text/number input in cell | `BaseGridEditor` | | Date picker input used in both grid and forms | `BaseGridEditorCVA` | | Dropdown/autocomplete with floating panel | `BaseOverlayEditor` | | Custom column filter UI | `BaseFilterPanel` | | Date picker overlay + form control | Compose: extend `BaseOverlayEditor` and implement `ControlValueAccessor` manually | ## Imports All base classes are exported from the main package: ```typescript import { BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, type OverlayPosition } from '@toolbox-web/grid-angular/features/editing'; import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering'; ``` --- # Angular Integration > Install and configure @toolbox-web/grid-angular — feature inputs, renderers, editors, events, inject functions, and Angular-specific patterns. The `@toolbox-web/grid-angular` package provides Angular integration for the `<tbw-grid>` data grid component. :::note[Where to find feature docs] This page covers Angular-specific setup and APIs (feature inputs, inject functions, Angular directives). Core grid features and plugins — cell renderers, editors, events, master-detail, sorting, filtering, selection, etc. — are documented on the [Core](https://toolboxjs.com/grid/core.md) and [Plugins](https://toolboxjs.com/grid/plugins.md) pages, each with an Angular tab and runnable demos. ::: ## Compatibility | Angular version | Support level | | --------------- | ------------- | | 21 | **Tested** — used in demos and CI | | 20 | **Tested** | | 17 – 19 | Supported (minimum peer dependency) | | < 17 | Not supported — adapter uses `input()` / `output()` signal APIs | ## Installation #### npm ```bash npm install @toolbox-web/grid @toolbox-web/grid-angular ``` #### yarn ```bash yarn add @toolbox-web/grid @toolbox-web/grid-angular ``` #### pnpm ```bash pnpm add @toolbox-web/grid @toolbox-web/grid-angular ``` #### bun ```bash bun add @toolbox-web/grid @toolbox-web/grid-angular ``` ## Setup No bootstrap step is required. Importing `@toolbox-web/grid-angular` (or any of its `features/*` entries) registers the `<tbw-grid>` custom element as a side effect. Each feature is enabled per-component by importing its directive — see [Two Ways to Configure Features](#two-ways-to-configure-features) below. ```typescript // Optional: if you also want to use <tbw-grid> in plain HTML templates // outside an Angular component (rare), add this once in main.ts: import '@toolbox-web/grid'; ``` ## Two Ways to Configure Features You can enable plugins on `<tbw-grid>` with one of two patterns. Pick whichever fits the component — they can be mixed. | Pattern | Best for | |---------|----------| | **`gridConfig.features`** (object literal) | Configuration-driven apps; the entire grid setup lives in one object | | **Per-feature directive** (e.g. `GridFilteringDirective`) | Template-driven apps that want signal inputs/outputs **and** the smallest bundle | :::note[Upgrading from v2?] The per-feature inputs/outputs that used to live on the central `Grid` directive (binding `[filtering]` without importing `GridFilteringDirective`) were **removed in v3**. The migration is a one-line `imports` addition per feature with zero template changes — see the [v3 migration guide](https://toolboxjs.com/grid/guides/migration-v3.md). ::: ### Recommended: Per-Feature Directives Each feature ships its own attribute-selector directive (`GridFilteringDirective`, `GridSelectionDirective`, …) from its secondary entry. Add the directive to the component's `imports` and bind exactly the same `[input]` / `(output)` you used before: ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering'; import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection'; @Component({ imports: [Grid, GridFilteringDirective, GridSelectionDirective], template: ` <tbw-grid [rows]="rows" [filtering]="true" (filterChange)="onFilter($event)" [selection]="'range'" /> `, }) export class MyGrid {/* … */} ``` **Why this is the preferred path:** - **Tree-shakeable typed surface** — the feature's `input()` / `output()` definitions ship inside the feature's own bundle. Apps that don't import the directive don't pay for its compiled metadata in the core bundle. - **Compile-time safety** — Angular errors with `Can't bind to 'filtering' since it isn't a known property of 'tbw-grid'` if you forget the directive (stronger than React/Vue, which silently drop unknown props). - **Same bindings, same demos** — the directive's selector matches the plain attributes (`tbw-grid[filtering], tbw-grid[filterChange]`), so the template markup is identical no matter which pattern you came from. ## Basic Usage The simplest way to use the grid is with the `Grid` directive and per-feature directives: ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection'; import { GridEditingDirective } from '@toolbox-web/grid-angular/features/editing'; import { GridMultiSortDirective } from '@toolbox-web/grid-angular/features/multi-sort'; import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering'; import type { ColumnConfig } from '@toolbox-web/grid-angular'; interface Employee { id: number; name: string; department: string; salary: number; } @Component({ selector: 'app-employee-grid', imports: [Grid, GridSelectionDirective, GridEditingDirective, GridMultiSortDirective, GridFilteringDirective], template: ` <tbw-grid [rows]="employees" [columns]="columns" [selection]="'range'" [editing]="'dblclick'" [multiSort]="true" [filtering]="{ debounceMs: 200 }" style="height: 400px; display: block;" /> `, }) export class EmployeeGridComponent { employees: Employee[] = [ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Charlie', department: 'Sales', salary: 85000 }, ]; columns: ColumnConfig<Employee>[] = [ { field: 'id', header: 'ID', type: 'number', width: 70 }, { field: 'name', header: 'Name', editable: true, sortable: true }, { field: 'department', header: 'Department', editable: true, sortable: true }, { field: 'salary', header: 'Salary', type: 'number', format: (v: number) => '$' + v.toLocaleString() }, ]; } ``` ## Feature Input Reference Each feature exposes an input + (optional) output. The recommended way to use them is to import the matching **per-feature directive** alongside `Grid` (left column). The same `[input]` / `(output)` bindings are also accepted directly on `Grid` in v1.x for backward compatibility, but those bindings on `Grid` are `@deprecated` and will be removed in v2.0 — see the [Grid directive API](https://toolboxjs.com/grid/angular/api/directives/grid.md). | Per-Feature Directive (recommended) | Input | Feature Import | Description | |-------------------------------------|-------|----------------|-------------| | [`GridSelectionDirective`](https://toolboxjs.com/grid/angular/api/directives/gridselectiondirective.md) | `[selection]` | `features/selection` | Cell, row, or range selection | | [`GridEditingDirective`](https://toolboxjs.com/grid/angular/api/directives/grideditingdirective.md) | `[editing]` | `features/editing` | Inline cell editing | | [`GridMultiSortDirective`](https://toolboxjs.com/grid/angular/api/directives/gridmultisortdirective.md) | `[multiSort]` | `features/multi-sort` | Multi-column sorting | | [`GridFilteringDirective`](https://toolboxjs.com/grid/angular/api/directives/gridfilteringdirective.md) | `[filtering]` | `features/filtering` | Column filtering | | [`GridClipboardDirective`](https://toolboxjs.com/grid/angular/api/directives/gridclipboarddirective.md) | `[clipboard]` | `features/clipboard` | Copy/paste support | | [`GridContextMenuDirective`](https://toolboxjs.com/grid/angular/api/directives/gridcontextmenudirective.md) | `[contextMenu]` | `features/context-menu` | Right-click context menu | | [`GridReorderColumnsDirective`](https://toolboxjs.com/grid/angular/api/directives/gridreordercolumnsdirective.md) | `[reorderColumns]` | `features/reorder-columns` | Column drag-to-reorder | | [`GridVisibilityDirective`](https://toolboxjs.com/grid/angular/api/directives/gridvisibilitydirective.md) | `[visibility]` | `features/visibility` | Column visibility panel | | [`GridPinnedColumnsDirective`](https://toolboxjs.com/grid/angular/api/directives/gridpinnedcolumnsdirective.md) | `[pinnedColumns]` | `features/pinned-columns` | Sticky left/right columns | | [`GridPinnedRowsDirective`](https://toolboxjs.com/grid/angular/api/directives/gridpinnedrowsdirective.md) | `[pinnedRows]` | `features/pinned-rows` | Sticky top/bottom rows | | [`GridGroupingColumnsDirective`](https://toolboxjs.com/grid/angular/api/directives/gridgroupingcolumnsdirective.md) | `[groupingColumns]` | `features/grouping-columns` | Multi-level column headers | | [`GridGroupingRowsDirective`](https://toolboxjs.com/grid/angular/api/directives/gridgroupingrowsdirective.md) | `[groupingRows]` | `features/grouping-rows` | Row grouping | | [`GridColumnVirtualizationDirective`](https://toolboxjs.com/grid/angular/api/directives/gridcolumnvirtualizationdirective.md) | `[columnVirtualization]` | `features/column-virtualization` | Virtualize columns for wide grids | | [`GridRowDragDropDirective`](https://toolboxjs.com/grid/angular/api/directives/gridrowdragdropdirective.md) | `[rowDragDrop]` / `[reorderRows]` | `features/row-drag-drop` | Row drag-and-drop (within and across grids) | | [`GridTreeDirective`](https://toolboxjs.com/grid/angular/api/directives/gridtreedirective.md) | `[tree]` | `features/tree` | Hierarchical tree view | | [`GridMasterDetailDirective`](https://toolboxjs.com/grid/angular/api/directives/gridmasterdetaildirective.md) | `[masterDetail]` | `features/master-detail` | Expandable detail rows | | [`GridResponsiveDirective`](https://toolboxjs.com/grid/angular/api/directives/gridresponsivedirective.md) | `[responsive]` | `features/responsive` | Card layout for narrow viewports | | [`GridTooltipDirective`](https://toolboxjs.com/grid/angular/api/directives/gridtooltipdirective.md) | `[tooltip]` | `features/tooltip` | Cell / header tooltips | | [`GridUndoRedoDirective`](https://toolboxjs.com/grid/angular/api/directives/gridundoredodirective.md) | `[undoRedo]` | `features/undo-redo` | Edit undo/redo | | [`GridExportDirective`](https://toolboxjs.com/grid/angular/api/directives/gridexportdirective.md) | `[export]` | `features/export` | CSV/Excel export | | [`GridPrintDirective`](https://toolboxjs.com/grid/angular/api/directives/gridprintdirective.md) | `[print]` | `features/print` | Print support | | [`GridPivotDirective`](https://toolboxjs.com/grid/angular/api/directives/gridpivotdirective.md) | `[pivot]` | `features/pivot` | Pivot table functionality | | [`GridServerSideDirective`](https://toolboxjs.com/grid/angular/api/directives/gridserversidedirective.md) | `[serverSide]` | `features/server-side` | Server-side data loading | | [`GridStickyRowsDirective`](https://toolboxjs.com/grid/angular/api/directives/gridstickyrowsdirective.md) | `[stickyRows]` | `features/sticky-rows` | Pin selected data rows below the header on scroll | **Core Config Inputs (no feature import needed):** | Input | Type | Description | |-------|------|-------------| | `[rows]` | `T[]` | Row data — omit when using `[serverSide]` | | `[columns]` | [`ColumnConfig`](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md)`[]` | Column definitions (alternative to `gridConfig.columns`) | | `[gridConfig]` | [`GridConfig`](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md) | Full grid configuration object | | `[loading]` | `boolean` | Show the grid's loading overlay | | `[fitMode]` | [`FitMode`](https://toolboxjs.com/grid/api/core/types/fitmode.md) | Column-fit strategy: `'stretch'` (default) or `'fixed'` | | `[customStyles]` | `string` | CSS injected into the grid — useful for custom renderer/editor styling | | `[sortable]` | `boolean` | Grid-wide sorting toggle (default: true) | | `[filterable]` | `boolean` | Grid-wide filtering toggle (default: true). Requires FilteringPlugin. | | `[selectable]` | `boolean` | Grid-wide selection toggle (default: true). Requires SelectionPlugin. | ## Programmatic Grid Access Use [`injectGrid()`](https://toolboxjs.com/grid/angular/api/utilities/injectgrid.md) for programmatic access to the grid. It provides a reactive API via signals — see [`InjectGridReturn`](https://toolboxjs.com/grid/angular/api/types/injectgridreturn.md) for the full return shape: ```typescript import { Component, effect } from '@angular/core'; import { Grid, injectGrid } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <tbw-grid [rows]="employees" [columns]="columns"></tbw-grid> <p>Grid ready: {{ grid.isReady() }}</p> <button (click)="refresh()">Force Layout</button> `, }) export class EmployeeGridComponent { grid = injectGrid(); employees = [/* ... */]; columns = [/* ... */]; constructor() { effect(() => { if (this.grid.isReady()) { console.log('Grid element:', this.grid.element()); } }); } refresh() { this.grid.forceLayout(); } } ``` ## Feature-Scoped Inject Functions Feature imports export **scoped inject functions** for type-safe programmatic access to plugin functionality — no direct plugin references needed. ```typescript import { GridExportDirective } from '@toolbox-web/grid-angular/features/export'; import { injectGridExport } from '@toolbox-web/grid-angular/features/export'; import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid, GridExportDirective], template: ` <div class="toolbar"> <button (click)="exportCsv()" [disabled]="gridExport.isExporting()">Export CSV</button> </div> <tbw-grid [rows]="employees" [columns]="columns" [export]="true" /> `, }) export class ExportGridComponent { employees = [/* ... */]; columns = [/* ... */]; gridExport = injectGridExport(); exportCsv() { this.gridExport.exportToCsv('employees.csv'); } } ``` ### Available Inject Functions See the **API Reference > Inject Functions** section for detailed method signatures, return types, and examples. All inject functions accept an optional `selector` parameter (defaults to `'tbw-grid'`) to target a specific grid when a component contains multiple grids: ```typescript // Target a specific grid by CSS selector gridSelection = injectGridSelection('tbw-grid.primary'); gridExport = injectGridExport('#secondary-grid'); ``` | Function | Import | Key Methods | |----------|--------|-------------| | [`injectGrid()`](https://toolboxjs.com/grid/angular/api/utilities/injectgrid.md) | `@toolbox-web/grid-angular` | `element`, `isReady`, `config`, `forceLayout`, `toggleGroup`, `registerStyles`, `visibleColumns` | | [`injectGridSelection()`](https://toolboxjs.com/grid/angular/api/features/injectgridselection.md) | `features/selection` | `selectAll`, `clearSelection`, `getSelection`, `selectedRowIndices` (Signal) | | [`injectGridFiltering()`](https://toolboxjs.com/grid/angular/api/features/injectgridfiltering.md) | `features/filtering` | `setFilter`, `clearAllFilters`, `getFilters`, `getFilteredRowCount` | | [`injectGridExport()`](https://toolboxjs.com/grid/angular/api/features/injectgridexport.md) | `features/export` | `exportToCsv`, `exportToExcel`, `exportToJson`, `isExporting` | | [`injectGridPrint()`](https://toolboxjs.com/grid/angular/api/features/injectgridprint.md) | `features/print` | `print`, `isPrinting` | | [`injectGridUndoRedo()`](https://toolboxjs.com/grid/angular/api/features/injectgridundoredo.md) | `features/undo-redo` | `undo`, `redo`, `canUndo` (Signal), `canRedo` (Signal) | ### Signal-Based Selection Example ```typescript import { Component, computed } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <tbw-grid [rows]="employees" [gridConfig]="gridConfig"></tbw-grid> <p>Selected: {{ selectedCount() }}</p> <button (click)="gridSelection.selectAll()">Select All</button> <button (click)="gridSelection.clearSelection()">Clear</button> `, }) export class EmployeeGridComponent { gridSelection = injectGridSelection(); selectedCount = computed(() => this.gridSelection.selectedRowIndices().length); gridConfig = { features: { selection: { mode: 'row', checkbox: true } }, }; } ``` ## Type-Level Defaults Register application-wide renderers and editors for specific data types using [`provideGridTypeDefaults()`](https://toolboxjs.com/grid/angular/api/utilities/providegridtypedefaults.md): ```typescript // app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideGridTypeDefaults } from '@toolbox-web/grid-angular'; import { CurrencyRendererComponent, DateRendererComponent } from './renderers'; export const appConfig: ApplicationConfig = { providers: [ provideGridTypeDefaults({ currency: { renderer: CurrencyRendererComponent }, date: { renderer: DateRendererComponent }, }), ], }; ``` For dynamic registration at runtime, inject [`GridTypeRegistry`](https://toolboxjs.com/grid/angular/api/utilities/gridtyperegistry.md): ```typescript import { inject } from '@angular/core'; import { GridTypeRegistry } from '@toolbox-web/grid-angular'; export class AppComponent { private registry = inject(GridTypeRegistry); ngOnInit() { this.registry.register('currency', { renderer: CurrencyCellComponent, }); } } ``` ## Custom Icons The grid supports two complementary ways to customize icons — see the [Theming Guide → Icon Customization](https://toolboxjs.com/grid/guides/theming.md#icon-customization) for both the CSS and JavaScript approaches: - **CSS variables** (`--tbw-icon-*`) — preferred for themes and static customization; no JavaScript needed. - **`gridConfig.icons`** — for dynamic icons, icon libraries, or `HTMLElement` instances; takes precedence over CSS. [`provideGridIcons()`](https://toolboxjs.com/grid/angular/api/utilities/providegridicons.md) is the Angular DI wrapper for the JS path — it injects `icons` into `gridConfig.icons` for every grid in the application: ```typescript // app.config.ts import { provideGridIcons } from '@toolbox-web/grid-angular'; export const appConfig = { providers: [ provideGridIcons({ sortAsc: '<svg>...</svg>', sortDesc: '<svg>...</svg>', filter: '<svg>...</svg>', }), ], }; ``` ## Manual Plugin Instantiation While feature inputs are recommended, you can instantiate plugins manually for custom configurations or third-party plugins: ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { SelectionPlugin, EditingPlugin, ClipboardPlugin } from '@toolbox-web/grid/all'; import type { GridConfig } from '@toolbox-web/grid-angular'; @Component({ selector: 'app-employee-grid', imports: [Grid], template: `<tbw-grid [rows]="employees" [gridConfig]="gridConfig"></tbw-grid>`, }) export class EmployeeGridComponent { employees = [/* ... */]; gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, ], plugins: [ new SelectionPlugin({ mode: 'range', checkbox: true }), new EditingPlugin({ editOn: 'dblclick' }), new ClipboardPlugin({ includeHeaders: true }), ], }; } ``` Feature inputs and manual plugins can be mixed — the grid merges them, with manual plugins added first. ## Angular-Specific Directives ### Custom Tool Panels Use the [`GridToolPanel`](https://toolboxjs.com/grid/angular/api/directives/gridtoolpanel.md) directive to declare a custom sidebar panel inline. Place a `<tbw-grid-tool-panel>` element inside the grid with an `<ng-template>` for its content — the template receives the grid element as its implicit context (and as `let-grid`): ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { GridToolPanel } from '@toolbox-web/grid-angular/features/shell'; @Component({ imports: [Grid, GridToolPanel], template: ` <tbw-grid [rows]="employees" [columns]="columns"> <tbw-grid-tool-panel id="stats" title="Statistics" icon="📊" [order]="10" > <ng-template let-grid> <div class="stats-panel"> <h3>Statistics</h3> <p>Total rows: {{ employees.length }}</p> </div> </ng-template> </tbw-grid-tool-panel> </tbw-grid> `, }) export class EmployeeGridComponent { employees = [/* ... */]; columns = [/* ... */]; } ``` **Inputs:** `id` (required), `title` (required), `icon`, `tooltip`, `order` (default `100`, lower = first). ### Header & Toolbar Content Inject Angular components into the grid's shell header (left zone) or toolbar (right zone) via the [`GridHeaderContent`](https://toolboxjs.com/grid/angular/api/directives/gridheadercontent.md) and [`GridToolbarContent`](https://toolboxjs.com/grid/angular/api/directives/gridtoolbarcontent.md) directives. These wrap the imperative `registerHeaderContent` / `registerToolbarContent` APIs and mount an `<ng-template>` so projected components retain full DI, change detection, and signal reactivity. ```typescript import { Component, signal } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import { GridHeaderContent, GridToolbarContent } from '@toolbox-web/grid-angular/features/shell'; @Component({ imports: [Grid, GridHeaderContent, GridToolbarContent, HeaderNavComponent, ToolbarNavComponent], template: ` <tbw-grid [rows]="rows()" [gridConfig]="config()"> <tbw-grid-header-content id="calendar-nav" [order]="0"> <ng-template> <app-header-nav [year]="year()" (yearChange)="onYearChange($event)" /> </ng-template> </tbw-grid-header-content> <tbw-grid-toolbar-content id="calendar-buttons" [order]="0"> <ng-template> <app-toolbar-nav (previous)="prev()" (today)="today()" (next)="next()" /> </ng-template> </tbw-grid-toolbar-content> </tbw-grid> `, }) export class CalendarGridComponent { year = signal(new Date().getFullYear()); onYearChange(year: number) { this.year.set(year); } } ``` **Inputs:** `id` (optional — auto-generated if omitted), `order` (default `100`, lower = first). Template bindings re-evaluate via Angular change detection without re-registering with the grid. ## Server-Side Data Loading The full data-source contract, sort/filter modes, prefetching, cancellation and live-update patterns are documented on the [Server-Side plugin](https://toolboxjs.com/grid/plugins/server-side.md) page (it has runnable demos and an Angular tab in every example). The two Angular-specific points worth calling out here: 1. **Use the per-feature directive (recommended).** Add [`GridServerSideDirective`](https://toolboxjs.com/grid/angular/api/directives/gridserversidedirective.md) to your component's `imports` if you want to bind via `[serverSide]="…"` on `<tbw-grid>`. If you configure server-side through `[gridConfig]="{ features: { serverSide: … } }"` instead, no extra directive is needed — the deprecated `[serverSide]` binding on `Grid` itself still works in v1.x but will be removed in v2.0. 2. **Return the `HttpClient` `Observable` directly from `getRows`.** No `firstValueFrom`, no manual `takeUntil`. The grid subscribes once and calls `unsubscribe()` when the request is superseded (sort change, fast scrolling, `refresh()`); `HttpClient` cancels the underlying XHR in response, which is the only correct way to avoid stale-buffer races. ```typescript import { Component, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { map } from 'rxjs/operators'; import { Grid } from '@toolbox-web/grid-angular'; import { GridServerSideDirective } from '@toolbox-web/grid-angular/features/server-side'; import type { GridConfig } from '@toolbox-web/grid-angular'; import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; @Component({ selector: 'app-employee-grid', imports: [Grid, GridServerSideDirective], template: `<tbw-grid [gridConfig]="gridConfig" style="height: 400px; display: block;"></tbw-grid>`, }) export class EmployeeGridComponent { private http = inject(HttpClient); private dataSource: ServerSideDataSource<Employee> = { getRows: (params) => { const httpParams = new HttpParams() .set('offset', params.startNode) .set('limit', params.endNode - params.startNode); return this.http .get<{ items: Employee[]; total: number }>('/api/employees', { params: httpParams }) .pipe(map((data) => ({ rows: data.items, totalNodeCount: data.total }))); }, }; gridConfig: GridConfig<Employee> = { columns: [/* … */], features: { serverSide: { dataSource: this.dataSource } }, }; } ``` > **Caching:** Angular has no built-in equivalent to TanStack Query's request cache. If you need cross-component caching, deduplication or `staleTime`, wrap the `HttpClient` call in your own service (e.g. with `shareReplay`), or evaluate [`@tanstack/angular-query-experimental`](https://tanstack.com/query/latest/docs/framework/angular/overview) — note the `experimental` tag. ## Dynamic Row Updates ```typescript import { Component, signal } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid], template: ` <tbw-grid [rows]="employees()" [columns]="columns"></tbw-grid> <button (click)="addEmployee()">Add</button> <button (click)="removeFirst()">Remove First</button> `, }) export class EmployeeGridComponent { employees = signal([ { id: 1, name: 'Alice', department: 'Engineering' }, { id: 2, name: 'Bob', department: 'Design' }, ]); columns = [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, ]; private nextId = 3; addEmployee() { this.employees.update(rows => [ ...rows, { id: this.nextId++, name: 'New Employee', department: 'TBD' }, ]); } removeFirst() { this.employees.update(rows => rows.slice(1)); } } ``` > **Tip**: Use Angular signals for rows. The grid detects the new array reference and re-renders efficiently. ### Smart Row Diffing When `getRowId` is configured in `gridConfig`, the adapter automatically diffs the previous and incoming `rows` signal value on every update. If only **values** changed — the row count and every row ID are the same — the adapter routes through `updateRows()` internally rather than replacing the whole array: - **Lighter render** — only the `VIRTUALIZATION` phase runs instead of the full `ROWS` phase. - **Selection preserved** — the `SelectionPlugin` only clears selection when `sourceRowCount` changes, so value-only updates keep any active selection and scroll position intact. Structural changes — rows added, removed, or **reordered**, or an ID appearing/disappearing — fall back to full replacement as before. The smart diff only runs when the row IDs match in the same order. ```typescript @Component({ template: `<tbw-grid [rows]="employees()" [gridConfig]="config"></tbw-grid>`, }) export class EmployeeGridComponent { // ✅ Enable smart diffing via getRowId config: GridConfig = { getRowId: (r: Employee) => String(r.id) }; employees = signal([ { id: 1, name: 'Alice', status: 'active' }, { id: 2, name: 'Bob', status: 'active' }, ]); // When update() produces the same count and same IDs, // the adapter calls updateRows() — selection stays. markInactive(id: number) { this.employees.update(rows => rows.map(e => e.id === id ? { ...e, status: 'inactive' } : e) ); } } ``` Without `gridConfig.getRowId` the behaviour is unchanged — every signal change replaces `grid.rows`. ## Performance Tips ### OnPush Change Detection ```typescript @Component({ changeDetection: ChangeDetectionStrategy.OnPush, // ... }) export class EmployeeGridComponent { } ``` ### Define Columns Once ```typescript // ✅ Good — assigned once readonly columns: ColumnConfig[] = [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, ]; // ❌ Bad — getter creates new array every check get columns() { return [{ field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }]; } ``` ## Troubleshooting ### Feature input not working Ensure you imported the feature side-effect in your `main.ts`: ```typescript // main.ts — must be imported before bootstrap import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection'; ``` ### "is not a known element" errors The `@toolbox-web/grid-angular` adapter provides directives that register all `<tbw-*>` elements with Angular's template compiler — no `CUSTOM_ELEMENTS_SCHEMA` needed. Import the directive that matches the element used in your template: ```typescript import { Grid, TbwGridColumn } from '@toolbox-web/grid-angular'; @Component({ imports: [Grid, TbwGridColumn], // Grid → <tbw-grid>, TbwGridColumn → <tbw-grid-column> // ... }) ``` ### Grid not rendering The grid needs a defined height for virtualization. See [Troubleshooting → Height & Virtualization](https://toolboxjs.com/grid/guides/troubleshooting.md#height--virtualization) for solutions. ### Row updates not reflected The grid detects updates via reference equality. Always assign a new array: ```typescript // ✅ New reference — grid re-renders this.employees.update(rows => [...rows, newRow]); // ❌ Same reference — grid won't detect change this.employees().push(newRow); ``` --- # Reactive Forms Integration > Bind @toolbox-web/grid to Angular FormArray — cell-level validation, dirty tracking, lazy form binding, and automatic validation styling. The `@toolbox-web/grid-angular` package integrates with Angular Reactive Forms, allowing the grid to act as a form-bound component with cell-level validation. > **Pair with the editing directive.** [`GridFormArray`](https://toolboxjs.com/grid/angular/api/directives/gridformarray.md) and [`GridLazyForm`](https://toolboxjs.com/grid/angular/api/directives/gridlazyform.md) are themselves form-binding directives — they do **not** turn editing on. To allow cell edits, also add [`GridEditingDirective`](https://toolboxjs.com/grid/angular/api/directives/grideditingdirective.md) (from `@toolbox-web/grid-angular/features/editing`) to the component's `imports`, **or** configure editing through `[gridConfig]="{ features: { editing: … } }"`. ## Overview You can bind a grid to a `FormArray` using the `[formArray]` directive. This enables: - **Two-way data binding** - FormArray value changes update the grid, and cell edits update the FormArray - **Validation state** - Access cell-level and row-level validation - **Dirty/touched tracking** - Know when users have interacted with the grid - **FormControl in editors** - Custom editors can bind directly to cell-level FormControls ## Usage with FormArray Use a `FormArray` of `FormGroup`s for full validation support. This exposes cell-level `FormControl`s in the editor context. > **Directive:** [`GridFormArray`](https://toolboxjs.com/grid/angular/api/directives/gridformarray.md) · binds via the `[formArray]` input on `<tbw-grid>`. ```typescript import { Component, inject, input, output } from '@angular/core'; import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators, AbstractControl } from '@angular/forms'; import { Grid, TbwRenderer } from '@toolbox-web/grid-angular'; import { GridEditingDirective, GridFormArray, TbwEditor } from '@toolbox-web/grid-angular/features/editing'; // Custom validated input editor @Component({ selector: 'app-validated-input', standalone: true, imports: [ReactiveFormsModule, GridEditingDirective], template: ` @if (control()) { <input [formControl]="control()" [class.is-invalid]="control()!.invalid && control()!.touched" /> @if (control()!.invalid && control()!.touched) { <small class="error">{{ getErrorMessage() }}</small> } } `, styles: ` .is-invalid { border-color: red; } .error { color: red; font-size: 0.8em; } ` }) export class ValidatedInputComponent { control = input<AbstractControl>(); commit = output<string>(); getErrorMessage(): string { const ctrl = this.control(); if (ctrl?.hasError('required')) return 'Required'; if (ctrl?.hasError('min')) return 'Value too low'; return 'Invalid'; } } @Component({ imports: [ Grid, GridFormArray, GridEditingDirective, TbwRenderer, TbwEditor, ReactiveFormsModule, ValidatedInputComponent ], template: ` <form [formGroup]="form"> <tbw-grid [formArray]="form.controls.employees" [gridConfig]="config" style="height: 400px; display: block;"> <tbw-grid-column field="age" editable> <span *tbwRenderer="let value">{{ value }}</span> <!-- The 'control' gives you the FormControl for this cell --> <app-validated-input *tbwEditor="let value; control as ctrl" [control]="ctrl" /> </tbw-grid-column> </tbw-grid> </form> <div class="validation-summary"> <p>Form Valid: {{ form.valid }}</p> <p>Dirty: {{ form.controls.employees.dirty }}</p> <p>Touched: {{ form.controls.employees.touched }}</p> <p>Errors: {{ getFormErrors() | json }}</p> </div> ` }) export class FormArrayExample { private fb = inject(FormBuilder); form = this.fb.group({ employees: this.fb.array([ this.fb.group({ name: ['Alice', Validators.required], age: [30, [Validators.required, Validators.min(18)]], }), this.fb.group({ name: ['Bob', Validators.required], age: [25, [Validators.required, Validators.min(18)]], }), ]) }); config = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'age', header: 'Age', editable: true }, ], features: { editing: true }, }; getFormErrors(): string[] { const errors: string[] = []; this.form.controls.employees.controls.forEach((group, idx) => { Object.keys(group.controls).forEach(field => { const ctrl = group.get(field); if (ctrl?.errors) { errors.push(`Row ${idx}, ${field}: ${JSON.stringify(ctrl.errors)}`); } }); }); return errors; } } ``` ## Variable Row Heights with FormArray When using variable row heights with FormArray, you **must** configure `rowId`. This is because `getRawValue()` returns new plain objects that don't match the original row references, breaking the WeakMap-based height cache. ```typescript gridConfig: GridConfig = { // REQUIRED: Enables height cache to survive FormArray updates rowId: 'id', rowHeight: (row) => row.hasValidationErrors ? undefined : 32, columns: [/* ... */], }; ``` Without `rowId`, each `getRawValue()` call creates new objects, causing all row heights to be re-measured unnecessarily. ## Row-Level Validation When using `FormArray` with `FormGroup`s, you can access row-level validation state via `getFormArrayContext()` (re-exported from [`GridFormArray`](https://toolboxjs.com/grid/angular/api/directives/gridformarray.md)). The returned [`FormArrayContext`](https://toolboxjs.com/grid/angular/api/types/formarraycontext.md) exposes: ```typescript import { getFormArrayContext } from '@toolbox-web/grid-angular/features/editing'; // Get validation context from grid element const context = getFormArrayContext(gridElement); if (context?.hasFormGroups) { // Row-level state const isRowValid = context.isRowValid(0); // All controls valid? const isRowTouched = context.isRowTouched(0); // Any control touched? const isRowDirty = context.isRowDirty(0); // Any control changed? // Aggregated errors for a row const errors = context.getRowErrors(0); // Returns: { name: { required: true }, age: { min: { min: 18, actual: 15 } } } // Direct control access const rowGroup = context.getRowFormGroup(0); // FormGroup for the row const cellCtrl = context.getControl(0, 'name'); // FormControl for one cell // Read-only data access const row = context.getRow(0); // Row object const allRows = context.getValue(); // Array of all row values // Programmatic mutation context.updateField(0, 'name', 'Alice'); // Patch one cell } ``` ## Editor Context Properties `*tbwEditor` exposes the core [`ColumnEditorContext`](https://toolboxjs.com/grid/api/core/interfaces/columneditorcontext.md) — `value`, `row`, `field`, `column`, `rowId`, `updateRow` and `onValueChange` — under the Angular names documented in [`GridEditorContext`](https://toolboxjs.com/grid/angular/api/types/grideditorcontext.md), where the commit and cancel callbacks are `onCommit` / `onCancel`. Two properties exist only in the Angular adapter: | Property | Type | Description | |-------------|------|-------------| | `$implicit` | `TValue` | Cell value, bound by `*tbwEditor="let value"` | | `control` | [`AbstractControl`](https://angular.dev/api/forms/AbstractControl) \| `undefined` | Cell `FormControl`, present only when bound to a `FormArray` + `FormGroup` | > **Auto-wiring:** the adapter listens for native `commit` / `cancel` `CustomEvent`s on the rendered component. If your editor dispatches them (or extends [`BaseGridEditor`](https://toolboxjs.com/grid/angular/base-classes.md) which does), you can omit the `(commit)="onCommit($event)"` binding entirely. ## Automatic Validation Syncing When using `GridFormArray`, Angular's FormControl validation is **automatically synced** to the grid's visual invalid styling. This means: 1. **After a cell is edited**, if the FormControl is invalid, the cell shows a red border/background 2. **When the FormControl becomes valid**, the invalid styling is automatically cleared 3. **On row-commit**, if the FormGroup has invalid controls, the commit is **prevented** and the row reverts to its original values This happens by default with `syncValidation="true"` (the default). You can disable it: ```html <!-- Disable automatic validation sync if you want manual control --> <tbw-grid [formArray]="rows" [syncValidation]="false" ... /> ``` ### How It Works The `GridFormArray` directive listens to `cell-commit` events and checks the corresponding FormControl's validity. If invalid, it calls `EditingPlugin.setInvalid()` with a human-readable error message derived from Angular's validation errors. Supported Angular validators are automatically translated to error messages: | Validator | Generated Message | |-----------|-------------------| | `required` | "This field is required" | | `minlength` | "Minimum length is X" | | `maxlength` | "Maximum length is X" | | `min` | "Minimum value is X" | | `max` | "Maximum value is X" | | `email` | "Invalid email address" | | `pattern` | "Invalid format" | | Custom | Uses `error.message` or "Validation error: key" | ### Custom Error Messages For custom error messages, provide a `message` property in your validator's error object: ```typescript // Custom validator with descriptive message function customValidator(control: AbstractControl): ValidationErrors | null { if (!isValid(control.value)) { return { customError: { message: 'Please enter a valid XYZ format' } }; } return null; } ``` ## CSS Classes Angular Forms automatically adds validation classes to the grid element: | Class | Description | |-------|-------------| | `.ng-valid` / `.ng-invalid` | Validation state | | `.ng-pristine` / `.ng-dirty` | Edit state | | `.ng-untouched` / `.ng-touched` | Touch state | | `.form-disabled` | Added when control is disabled | ```css tbw-grid.ng-invalid.ng-touched { border: 2px solid red; } tbw-grid.form-disabled { opacity: 0.6; pointer-events: none; } ``` ## Cell-Level Invalid Styling When Angular validation fails, the grid visually highlights invalid cells using CSS custom properties: ```css tbw-grid { /* Customize invalid cell appearance */ --tbw-invalid-bg: #fef2f2; --tbw-invalid-border-color: #ef4444; } ``` This integrates with the EditingPlugin's validation system, meaning Angular's `Validators.required`, `Validators.email`, etc. automatically show visual feedback in the grid. --- ## Lazy Form Binding (GridLazyForm) For large datasets, `GridFormArray` can be slow because it creates **all FormGroups upfront**. With 200 rows and 22 fields, that's 4,400 FormControl instances! `GridLazyForm` solves this by creating FormGroups **on-demand** when editing starts—typically reducing control count by 100x. > **Directive:** [`GridLazyForm`](https://toolboxjs.com/grid/angular/api/directives/gridlazyform.md) · [`LazyFormFactory`](https://toolboxjs.com/grid/angular/api/types/lazyformfactory.md) · [`RowFormChangeEvent`](https://toolboxjs.com/grid/angular/api/types/rowformchangeevent.md) ### When to Use Each Directive | Directive | Best For | FormGroups Created | |-----------|----------|-------------------| | `GridFormArray` | Small datasets (fewer than 50 rows), full-grid editing, Excel-like UX | All upfront | | `GridLazyForm` | Large datasets (100+ rows), typical CRUD apps | Only when editing | ### Performance Comparison | Rows | GridFormArray (22 fields) | GridLazyForm | |------|--------------------------|--------------| | 100 | 2,200 controls | ~22 controls | | 500 | 11,000 controls | ~22 controls | | 1,000 | 22,000 controls | ~22 controls | ### Basic Usage ```typescript import { Component, inject, signal } from '@angular/core'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { Grid } from '@toolbox-web/grid-angular'; import { GridEditingDirective, GridLazyForm, TbwEditor } from '@toolbox-web/grid-angular/features/editing'; interface Employee { id: number; firstName: string; lastName: string; salary: number; } @Component({ imports: [Grid, GridLazyForm, GridEditingDirective, TbwEditor, ReactiveFormsModule], template: ` <tbw-grid [rows]="employees()" [lazyForm]="createRowForm" [gridConfig]="gridConfig"> <tbw-grid-column field="firstName"> <input *tbwEditor="let _; control as ctrl" [formControl]="ctrl" [class.is-invalid]="ctrl?.invalid && ctrl?.touched" /> </tbw-grid-column> <tbw-grid-column field="salary"> <input *tbwEditor="let _; control as ctrl" type="number" [formControl]="ctrl" /> </tbw-grid-column> </tbw-grid> ` }) export class LazyFormExample { private fb = inject(FormBuilder); employees = signal<Employee[]>([ { id: 1, firstName: 'Alice', lastName: 'Smith', salary: 75000 }, { id: 2, firstName: 'Bob', lastName: 'Jones', salary: 82000 }, ]); // Factory called ONLY when a row enters edit mode createRowForm = (employee: Employee): FormGroup => this.fb.group({ // Only include EDITABLE fields - skip read-only fields like 'id' firstName: [employee.firstName, Validators.required], lastName: [employee.lastName, [Validators.required, Validators.minLength(2)]], salary: [employee.salary, [Validators.required, Validators.min(0)]], }); gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'firstName', header: 'First Name', editable: true }, { field: 'lastName', header: 'Last Name', editable: true }, { field: 'salary', header: 'Salary', editable: true, type: 'number' }, ], features: { editing: true }, }; } ``` ### Configuration Options | Input | Type | Default | Description | |-------|------|---------|-------------| | `[lazyForm]` | `(row: T) => FormGroup` | Required | Factory function to create FormGroups | | `[syncValidation]` | `boolean` | `true` | Sync Angular validation to grid styling | | `[keepFormGroups]` | `boolean` | `false` | Keep FormGroups cached after edit ends | | Output | Type | Description | |--------|------|-------------| | `(rowFormChange)` | `RowFormChangeEvent` | Emitted when form values change | ### Form Factory Best Practices ```typescript // ✅ Good: Only include editable fields createRowForm = (row: Employee): FormGroup => this.fb.group({ firstName: [row.firstName, Validators.required], lastName: [row.lastName], salary: [row.salary, [Validators.min(0)]], }); // ❌ Bad: Including read-only fields wastes memory createRowForm = (row: Employee): FormGroup => this.fb.group({ id: [{ value: row.id, disabled: true }], // Unnecessary! email: [{ value: row.email, disabled: true }], // Unnecessary! firstName: [row.firstName], // ... }); ``` ### Keeping FormGroups Between Edits By default, FormGroups are cleaned up when a row exits edit mode. Set `[keepFormGroups]="true"` to preserve dirty/touched state: ```html <!-- FormGroups persist between edit sessions --> <tbw-grid [rows]="employees()" [lazyForm]="createRowForm" [keepFormGroups]="true" [gridConfig]="gridConfig"> </tbw-grid> ``` ### Listening to Form Changes The `(rowFormChange)` output emits a [`RowFormChangeEvent`](https://toolboxjs.com/grid/angular/api/types/rowformchangeevent.md) every time the row's `FormGroup` value or status changes. The event includes `rowIndex`, `rowId?`, `row`, `formGroup`, `values`, `valid`, and `dirty`. ```typescript import { Component } from '@angular/core'; import { Grid } from '@toolbox-web/grid-angular'; import type { RowFormChangeEvent } from '@toolbox-web/grid-angular/features/editing'; @Component({ imports: [Grid], template: ` <tbw-grid [rows]="employees()" [lazyForm]="createRowForm" (rowFormChange)="onFormChange($event)" [gridConfig]="gridConfig"> </tbw-grid> <div *ngIf="lastChange"> <p>Last edit: Row {{ lastChange.rowIndex }}</p> <p>Valid: {{ lastChange.valid }}</p> <p>Dirty: {{ lastChange.dirty }}</p> </div> ` }) export class FormChangeExample { lastChange: RowFormChangeEvent<Employee> | null = null; onFormChange(event: RowFormChangeEvent<Employee>) { this.lastChange = event; console.log('Form changed:', event.values); // Auto-save dirty, valid forms if (event.dirty && event.valid) { this.autoSave(event.row, event.values); } } } ``` ### Programmatic Access ```typescript import { viewChild } from '@angular/core'; import { GridLazyForm } from '@toolbox-web/grid-angular/features/editing'; @Component({ /* ... */ }) export class MyComponent { lazyForm = viewChild.required(GridLazyForm<Employee>); validateBeforeSave() { // Validate all currently cached FormGroups if (!this.lazyForm().validateAll()) { console.log('Some rows are invalid'); return false; } return true; } getEditedRows() { // Get all rows that have been edited const formGroups = this.lazyForm().getAllFormGroups(); console.log('Edited rows:', formGroups.size); return formGroups; } resetAfterSave() { // Clear all cached FormGroups after successful save this.lazyForm().clearAllFormGroups(); } } ``` ### Full-Grid Editing with FormArray `GridFormArray` pairs naturally with `mode: 'grid'`—the spreadsheet-like editing mode where **all editable cells display their editors at once**. ```typescript gridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'age', header: 'Age', editable: true, type: 'number' }, ], features: { editing: { mode: 'grid' } }, }; ``` This enables: - **All editors visible immediately** — no click-to-edit; every editable cell shows its input - **Tab between cells** across the entire grid - **Arrow-key navigation** — press Escape to leave an input, then navigate with arrow keys; press Enter to re-enter the input - **Cell-level revert on Escape** — reverts the cell to its pre-edit value and resets the corresponding `FormControl` - **Reactive validation syncing** — `GridFormArray` subscribes to each `FormControl`'s `statusChanges` so invalid styling updates in real time as the user types - **Bulk validation before save** — call `form.valid` or inspect `FormArray`-level errors - **FormArray-level operations** — `push`, `removeAt`, etc. update the grid automatically #### How `GridFormArray` Handles Grid Mode When `syncValidation` is enabled (the default) and the `EditingPlugin` is in `mode: 'grid'`: 1. **Initial validation** — pre-existing validation errors are shown as soon as the grid renders 2. **Reactive updates** — each `FormControl`'s `statusChanges` is subscribed so the grid marks/clears invalid cells automatically 3. **Escape (cell-cancel)** — the `cell-cancel` event resets the `FormControl` to its previous value, keeping Angular form state in sync ```typescript @Component({ imports: [Grid, GridFormArray, GridEditingDirective, TbwEditor, ReactiveFormsModule], template: ` <form [formGroup]="form"> <tbw-grid [formArray]="form.controls.employees" [gridConfig]="gridConfig" style="height: 400px; display: block;"> <tbw-grid-column field="name" editable> <input *tbwEditor="let _; control as ctrl" [formControl]="ctrl" /> </tbw-grid-column> <tbw-grid-column field="age" editable> <input *tbwEditor="let _; control as ctrl" type="number" [formControl]="ctrl" /> </tbw-grid-column> </tbw-grid> </form> <button [disabled]="form.invalid" (click)="save()">Save All</button> ` }) export class GridModeFormArrayExample { private fb = inject(FormBuilder); form = this.fb.group({ employees: this.fb.array([ this.fb.group({ name: ['Alice', Validators.required], age: [30, [Validators.required, Validators.min(18)]] }), this.fb.group({ name: ['Bob', Validators.required], age: [25, [Validators.required, Validators.min(18)]] }), ]) }); gridConfig = { columns: [ { field: 'name', header: 'Name', editable: true }, { field: 'age', header: 'Age', editable: true, type: 'number' }, ], features: { editing: { mode: 'grid' } }, }; save() { console.log('Saving:', this.form.controls.employees.getRawValue()); } } ``` For typical single-row CRUD workflows, `GridLazyForm` with the default `mode: 'row'` remains the more efficient choice. --- # React Integration > Install and configure @toolbox-web/grid-react — feature props, JSX renderers, editors, hooks, and event handling. The `@toolbox-web/grid-react` package provides React integration for the `<tbw-grid>` data grid component. :::note[Where to find feature docs] This page covers React-specific setup and APIs (feature props, hooks, providers). Core grid features and plugins — cell renderers, editors, events, master-detail, sorting, filtering, selection, etc. — are documented on the [Core](https://toolboxjs.com/grid/core.md) and [Plugins](https://toolboxjs.com/grid/plugins.md) pages, each with a React tab and runnable demos. ::: ## Compatibility | React version | Support level | | ------------- | ------------- | | 19 | **Tested** — used in demos and CI | | 18 | **Tested** | | < 18 | Not supported (minimum peer dependency) | ## Installation #### npm ```bash npm install @toolbox-web/grid @toolbox-web/grid-react ``` #### yarn ```bash yarn add @toolbox-web/grid @toolbox-web/grid-react ``` #### pnpm ```bash pnpm add @toolbox-web/grid @toolbox-web/grid-react ``` #### bun ```bash bun add @toolbox-web/grid @toolbox-web/grid-react ``` ## Setup No bootstrap step is required. Importing anything from `@toolbox-web/grid-react` (the `DataGrid` component, a hook, or any `features/*` entry) registers the `<tbw-grid>` custom element as a side effect — so you do **not** need a global `import '@toolbox-web/grid';` in `main.tsx`. Import runtime values (the component, hooks, providers) **and** the config types from `@toolbox-web/grid-react`. The adapter's `GridConfig` / `ColumnConfig` widen `renderer` / `editor` to accept JSX components — the base types in `@toolbox-web/grid` only accept `HTMLElement | string`, so importing them here breaks component renderers: ```tsx import { DataGrid } from '@toolbox-web/grid-react'; import type { GridConfig, ColumnConfig } from '@toolbox-web/grid-react'; ``` Reach into `@toolbox-web/grid` only for types the adapter doesn't re-export (event-detail types, enums, plugin option interfaces). Enable features with **co-located** side-effect imports — put them in the component file that renders the grid (see [Enabling Features with Props](#enabling-features-with-props) below), not in `main.tsx` or `App.tsx`. ## Basic Usage The simplest way to use the grid is with the `DataGrid` component: ```tsx import { useState } from 'react'; import { DataGrid } from '@toolbox-web/grid-react'; interface Employee { id: number; name: string; department: string; salary: number; } function EmployeeGrid() { const [employees] = useState<Employee[]>([ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Charlie', department: 'Sales', salary: 85000 }, ]); return ( <DataGrid rows={employees} columns={[ { field: 'id', header: 'ID', width: 70 }, { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number' }, ]} style={{ height: 400, display: 'block' }} /> ); } ``` ## Enabling Features with Props Features are enabled using **declarative props** combined with **side-effect imports**. This gives you clean JSX and tree-shakeable bundles. ### How It Works 1. **Import the feature** - A side-effect import registers the feature factory 2. **Use the prop** - DataGrid detects the prop and creates the plugin instance ```tsx // 1. Import features you need (once per feature) import '@toolbox-web/grid-react/features/selection'; import '@toolbox-web/grid-react/features/multi-sort'; import '@toolbox-web/grid-react/features/editing'; import '@toolbox-web/grid-react/features/filtering'; import { DataGrid } from '@toolbox-web/grid-react'; function EmployeeGrid() { return ( <DataGrid rows={employees} columns={columns} // 2. Just use the props - plugins are created automatically! selection="range" // SelectionPlugin with mode: 'range' multiSort // MultiSortPlugin editing="dblclick" // EditingPlugin with editOn: 'dblclick' filtering // FilteringPlugin with defaults /> ); } ``` ### Why Side-Effect Imports? - **Tree-shakeable** - Only the features you import are bundled - **Synchronous** - No loading states, no HTTP requests, no spinners - **Type-safe** - Full TypeScript support for feature props - **Clean JSX** - No `plugins: [new SelectionPlugin({ mode: 'range' })]` boilerplate ### Available Features | Import | Prop | Example | |--------|------|---------| | `features/selection` | `selection` | `selection="range"` or `selection={{ mode: 'row', checkbox: true }}` | | `features/multi-sort` | `multiSort` | `multiSort` or `multiSort={{ maxSortColumns: 3 }}` | | `features/filtering` | `filtering` | `filtering` or `filtering={{ debounceMs: 200 }}` | | `features/editing` | `editing` | `editing="dblclick"` or `editing="click"` | | `features/clipboard` | `clipboard` | `clipboard` (requires selection) | | `features/undo-redo` | `undoRedo` | `undoRedo` (requires editing) | | `features/context-menu` | `contextMenu` | `contextMenu` | | `features/reorder-columns` | `reorderColumns` | `reorderColumns` (column drag-to-reorder) | | `features/visibility` | `visibility` | `visibility` (column visibility panel) | | `features/pinned-columns` | `pinnedColumns` | `pinnedColumns` | | `features/grouping-columns` | `groupingColumns` | `groupingColumns` | | `features/grouping-rows` | `groupingRows` | `groupingRows={{ groupOn: (row) => row.department }}` | | `features/tree` | `tree` | `tree={{ childrenField: 'children' }}` | | `features/master-detail` | `masterDetail` | `masterDetail` (use with `<GridDetailPanel>`) | | `features/responsive` | `responsive` | `responsive` (card layout on mobile) | | `features/export` | `export` | `export` | | `features/print` | `print` | `print` | | `features/pinned-rows` | `pinnedRows` | `pinnedRows` or `pinnedRows={{ position: 'bottom' }}` | | `features/column-virtualization` | `columnVirtualization` | `columnVirtualization` | | `features/pivot` | `pivot` | `pivot={{ rowFields: ['category'], valueField: 'sales' }}` | | `features/server-side` | `serverSide` | `serverSide={{ pageSize: 50 }}` | | `features/sticky-rows` | `stickyRows` | `stickyRows={{ isSticky: 'isSection' }}` | | `features/row-drag-drop` | `rowDragDrop` | `rowDragDrop` (cross-grid / external row drag-and-drop) | | `features/tooltip` | `tooltip` | `tooltip` (cell / header tooltips) | ### Core Config Props (no import needed) These props control core grid behaviour and don't require feature imports. See [`DataGridProps`](https://toolboxjs.com/grid/react/api/types/datagridprops.md) for the full prop surface and [`AllFeatureProps`](https://toolboxjs.com/grid/react/api/types/allfeatureprops.md) for every typed feature prop. | Prop | Type | Default | Description | |------|------|---------|-------------| | `rows` | `T[]` | `[]` | Row data — omit when using `serverSide` | | `columns` | [`ColumnConfig`](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md)`[]` | — | Column definitions (alternative to `gridConfig.columns`) | | `gridConfig` | [`GridConfig`](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md) | — | Full configuration object — always wrap in `useMemo` | | `ref` | [`Ref<DataGridRef>`](https://toolboxjs.com/grid/react/api/types/datagridref.md) | — | Imperative handle (also returned by `useGrid()`) | | `loading` | `boolean` | `false` | Show the grid's loading overlay | | `fitMode` | [`FitMode`](https://toolboxjs.com/grid/api/core/types/fitmode.md) | `'stretch'` | Column-fit strategy: `'stretch'` or `'fixed'` | | `customStyles` | `string` | — | CSS injected into the grid (renderer/editor styling) | | `sortable` | `boolean` | `true` | Grid-wide sorting toggle. Set `false` to disable all sorting. | | `filterable` | `boolean` | `true` | Grid-wide filtering toggle. Requires FilteringPlugin. | | `selectable` | `boolean` | `true` | Grid-wide selection toggle. Requires SelectionPlugin. | ```tsx // Disable sorting and selection at runtime <DataGrid sortable={false} selectable={false} selection="range" // Plugin loaded but disabled via selectable={false} /> ``` ### Import All Features (Development) For prototyping or when bundle size isn't critical: ```tsx // Import all features at once import '@toolbox-web/grid-react/features'; // Now all feature props work <DataGrid selection="range" multiSort filtering editing="dblclick" clipboard /> ``` ## Custom Tool Panels Use [`GridToolPanel`](https://toolboxjs.com/grid/react/api/components/gridtoolpanel.md) to declare a sidebar panel inline. The component renders a `<tbw-grid-tool-panel>` element in the grid's light DOM — the shell picks it up automatically, so **no `gridConfig.shell.toolPanels` registration is needed**. The `children` prop is a render function that receives a [`ToolPanelContext`](https://toolboxjs.com/grid/react/api/types/toolpanelcontext.md) (`{ grid }`). ```tsx import { DataGrid, GridToolPanel, type ToolPanelContext } from '@toolbox-web/grid-react'; function EmployeeGrid() { return ( <DataGrid rows={employees} columns={columns}> <GridToolPanel id="filters" title="Quick Filters" icon="🔍" order={10} > {({ grid }: ToolPanelContext) => ( <div style={{ padding: 16 }}> <h3>Quick Filters</h3> <label> <input type="checkbox" /> Active Only </label> </div> )} </GridToolPanel> </DataGrid> ); } ``` **Props:** `id` (required), `title` (required), `icon`, `tooltip`, `order` (default `100`, lower = first). ## Header & Toolbar Content Inject framework content into the grid's shell header (left zone) or toolbar (right zone) via [`GridHeaderContent`](https://toolboxjs.com/grid/react/api/components/gridheadercontent.md) and [`GridToolbarContent`](https://toolboxjs.com/grid/react/api/components/gridtoolbarcontent.md). These wrap the imperative `registerHeaderContent` / `registerToolbarContent` APIs so children render through React's normal reconciliation (state, context, hooks all work). ```tsx import { DataGrid, GridHeaderContent, GridToolbarContent } from '@toolbox-web/grid-react'; function CalendarGrid() { const [year, setYear] = useState(new Date().getFullYear()); return ( <DataGrid rows={rows} columns={columns}> <GridHeaderContent id="calendar-nav" order={0}> <HeaderNav year={year} onYearChange={setYear} /> </GridHeaderContent> <GridToolbarContent id="calendar-buttons" order={0}> <ToolbarNav onPrev={prev} onToday={today} onNext={next} /> </GridToolbarContent> </DataGrid> ); } ``` **Props:** `id` (optional — auto-generated if omitted), `order` (default `100`, lower = first). Children re-render in place when state changes without re-registering with the grid. ## Using the `useGrid` Hook The [`useGrid()`](https://toolboxjs.com/grid/react/api/hooks/usegrid.md) hook provides programmatic access to the grid — see [`UseGridReturn`](https://toolboxjs.com/grid/react/api/types/usegridreturn.md) for the full return shape. It exposes core grid operations only; **feature-specific operations (export, selection, filtering, etc.) live on the dedicated feature hooks** (`useGridSelection`, `useGridExport`, ...) listed below. ```tsx import { DataGrid, useGrid } from '@toolbox-web/grid-react'; function EmployeeGrid() { const { ref, // Pass to DataGrid element, // Direct grid element access isReady, // True once the grid has booted config, // Current effective GridConfig (snapshot) getConfig, // async: re-read effective config forceLayout, // async: force a layout pass toggleGroup, // async: toggle a group row by key getPlugin, // Get plugin instance by class getPluginByName, // Get plugin instance by registered name registerStyles, // Inject scoped CSS unregisterStyles, getVisibleColumns, // Filtered list of non-hidden columns } = useGrid<Employee>(); return ( <div> <button onClick={() => forceLayout()}>Force Layout</button> <button disabled={!isReady} onClick={async () => console.log(await getConfig())}> Log Config </button> <DataGrid ref={ref} rows={employees} columns={columns} /> </div> ); } ``` For selection, export, filtering, undo-redo, or print operations, use the feature hooks — they handle the per-plugin wiring: ```tsx import '@toolbox-web/grid-react/features/selection'; import '@toolbox-web/grid-react/features/export'; import { useGridSelection } from '@toolbox-web/grid-react/features/selection'; import { useGridExport } from '@toolbox-web/grid-react/features/export'; function Toolbar() { const { selectAll, clearSelection, getSelectedRows } = useGridSelection<Employee>(); const { exportToCsv, isExporting } = useGridExport(); return ( <div className="toolbar"> <button onClick={() => exportToCsv('employees.csv')} disabled={isExporting()}> Export CSV </button> <button onClick={selectAll}>Select All</button> <button onClick={() => console.log(getSelectedRows())}>Log Selection</button> <button onClick={clearSelection}>Clear</button> </div> ); } ``` ### Multiple Grids When a component contains multiple grids, pass a CSS selector to target a specific one: ```tsx import { useGrid } from '@toolbox-web/grid-react'; import { useGridSelection } from '@toolbox-web/grid-react/features/selection'; function MultiGridPage() { // Target grids by selector instead of ref/context const primaryGrid = useGrid('#primary-grid'); const selection = useGridSelection('#primary-grid'); return ( <div> <DataGrid id="primary-grid" rows={employees} selection="row" /> <DataGrid id="secondary-grid" rows={departments} /> </div> ); } ``` ## Type-Level Defaults Register application-wide renderers for data types using `GridTypeProvider` (see [`GridTypeProviderProps`](https://toolboxjs.com/grid/react/api/types/gridtypeproviderprops.md)) with a [`TypeDefaultsMap`](https://toolboxjs.com/grid/react/api/types/typedefaultsmap.md): ```tsx import { GridTypeProvider, DataGrid, type TypeDefaultsMap } from '@toolbox-web/grid-react'; // Define type defaults for your application const typeDefaults: TypeDefaultsMap = { currency: { format: (value: number) => '$' + value.toLocaleString(), renderer: ({ value }) => ( <span style={{ color: value < 0 ? 'red' : 'green' }}>{value}</span> ), }, date: { format: (value: string) => new Date(value).toLocaleDateString(), }, boolean: { renderer: ({ value }) => <span>{value ? '✅' : '❌'}</span>, }, }; // Wrap your app (or a section) with the provider function App() { return ( <GridTypeProvider defaults={typeDefaults}> <EmployeeGrid /> </GridTypeProvider> ); } // Now columns with these types use the defaults automatically function EmployeeGrid() { return ( <DataGrid rows={employees} columns={[ { field: 'salary', header: 'Salary', type: 'currency' }, // Uses currency format/renderer { field: 'hireDate', header: 'Hire Date', type: 'date' }, // Uses date format { field: 'isActive', header: 'Active', type: 'boolean' }, // Uses boolean renderer ]} /> ); } ``` ## Feature-Scoped Hooks Feature imports export **scoped hooks** for type-safe programmatic access to plugin functionality: ```tsx import '@toolbox-web/grid-react/features/export'; import { useGridExport } from '@toolbox-web/grid-react/features/export'; import { DataGrid } from '@toolbox-web/grid-react'; function EmployeeGrid() { const { exportToCsv, exportToExcel, isExporting } = useGridExport(); return ( <div> <button onClick={() => exportToCsv('employees.csv')} disabled={isExporting()}> Export CSV </button> <DataGrid rows={employees} columns={columns} export /> </div> ); } ``` ### Available Feature Hooks See the **API Reference > Hooks** section for detailed method signatures, return types, and examples. | Hook | Import | Key Methods | |------|--------|-------------| | [`useGridSelection()`](https://toolboxjs.com/grid/react/api/features/usegridselection.md) | `features/selection` | `selectAll`, `clearSelection`, `getSelection`, `getSelectedRows` | | [`useGridFiltering()`](https://toolboxjs.com/grid/react/api/features/usegridfiltering.md) | `features/filtering` | `setFilter`, `clearAllFilters`, `getFilters`, `getFilteredRowCount` | | [`useGridExport()`](https://toolboxjs.com/grid/react/api/features/usegridexport.md) | `features/export` | `exportToCsv`, `exportToExcel`, `exportToJson`, `isExporting` | | [`useGridPrint()`](https://toolboxjs.com/grid/react/api/features/usegridprint.md) | `features/print` | `print`, `isPrinting` | | [`useGridUndoRedo()`](https://toolboxjs.com/grid/react/api/features/usegridundoredo.md) | `features/undo-redo` | `undo`, `redo`, `canUndo`, `canRedo` | ## Overlay Editors (`useGridOverlay`) Custom React editors that open a popover, listbox, calendar, or other floating panel typically render that panel into a portal so it can escape the cell's overflow clipping. Without help, the grid sees a click into the portal as a click _outside_ the editor and commits-and-exits the row before the user can pick an option. The `useGridOverlay` hook bridges that gap by registering the panel with the grid as an external focus container — the React equivalent of Angular's `BaseOverlayEditor.initOverlay()`. Pair it with `aria-expanded` / `aria-controls` on the trigger so even editors that forget to call the hook get correct keyboard / pointer behaviour out of the box (the editing plugin honours those attributes as a generic fallback — see [issue #251](https://github.com/OysteinAmundsen/toolbox/issues/251)). ```tsx import { createPortal } from 'react-dom'; import { useId, useRef, useState } from 'react'; import { useGridOverlay, type GridEditorContext } from '@toolbox-web/grid-react'; function AutocompleteEditor({ value, commit, cancel }: GridEditorContext<MyRow>) { const [open, setOpen] = useState(false); const panelRef = useRef<HTMLDivElement | null>(null); const listboxId = useId(); // Registers panelRef.current with the grid while open=true; unregisters // automatically on close or unmount. useGridOverlay(panelRef, { open }); return ( <> <input role="combobox" aria-expanded={open} aria-controls={listboxId} defaultValue={String(value ?? '')} onClick={() => setOpen(true)} onKeyDown={(e) => { if (e.key === 'Escape') cancel(); }} /> {open && createPortal( <div ref={panelRef} id={listboxId} role="listbox" className="my-listbox"> {/* options that call commit(option) on click */} </div>, document.body, )} </> ); } ``` `useGridOverlay(panelRef, options?)` resolves the owning `<tbw-grid>` in this order: 1. `options.gridElement` if explicitly passed, 2. `panelRef.current.closest('tbw-grid')` for inline (non-portaled) panels, 3. The `GridElementContext` populated by `<DataGrid>` / `<GridProvider>`. The grid host is also exposed on `ColumnEditorContext.grid` for editors that need to call grid APIs directly without using the hook. ## Manual Plugin Instantiation Feature props are the recommended path. Use manual plugin instantiation only when you need a third-party plugin or a configuration the feature prop doesn't expose. Feature props and manual plugins can be mixed — the grid merges them. ```tsx import { useMemo } from 'react'; import { DataGrid, type GridConfig } from '@toolbox-web/grid-react'; import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection'; import { MyCustomPlugin } from './my-custom-plugin'; function EmployeeGrid() { const config = useMemo<GridConfig<Employee>>(() => ({ columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, ], plugins: [ new SelectionPlugin({ mode: 'range', checkbox: true }), new MyCustomPlugin({ /* third-party config */ }), ], }), []); return <DataGrid rows={employees} gridConfig={config} />; } ``` > **Important**: Always use `useMemo` for config objects to prevent re-creating them on every render. ## Custom Icons The grid supports two complementary ways to customize icons — see the [Theming Guide → Icon Customization](https://toolboxjs.com/grid/guides/theming.md#icon-customization) for both the CSS and JavaScript approaches: - **CSS variables** (`--tbw-icon-*`) — preferred for themes and static customization; no JavaScript needed. - **`gridConfig.icons`** — for dynamic icons, icon libraries, or `HTMLElement` instances; takes precedence over CSS. `GridIconProvider` is the React wrapper for the JS path — it injects `icons` into `gridConfig.icons` for every descendant `<DataGrid>`: ```tsx import { GridIconProvider, DataGrid } from '@toolbox-web/grid-react'; function App() { return ( <GridIconProvider icons={{ sortAsc: '↑', sortDesc: '↓' }}> <DataGrid rows={employees} columns={columns} /> </GridIconProvider> ); } ``` ## Server-Side Data Loading For backends that own paging / sorting / filtering, use the `serverSide` feature prop. Your `getRows` handler receives the current `sortModel`, `filterModel`, and block range — return rows and the total count. See [`ServerSideDataSource`](https://toolboxjs.com/grid/plugins/server-side/interfaces/serversidedatasource.md), [`GetRowsParams`](https://toolboxjs.com/grid/plugins/server-side/interfaces/getrowsparams.md), and [`GetRowsResult`](https://toolboxjs.com/grid/plugins/server-side/interfaces/getrowsresult.md) for the full contract — including the `params.signal` `AbortSignal` you can forward to `fetch()` for explicit cancellation. ```tsx import '@toolbox-web/grid-react/features/server-side'; import '@toolbox-web/grid-react/features/multi-sort'; import { useMemo } from 'react'; import { DataGrid } from '@toolbox-web/grid-react'; import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; function ServerSideGrid() { const dataSource = useMemo<ServerSideDataSource<Employee>>(() => ({ getRows: async ({ startNode, endNode, sortModel, filterModel, signal }) => { const params = new URLSearchParams({ from: String(startNode), to: String(endNode), }); if (sortModel?.length) { params.set( 'sort', sortModel.map((s) => `${s.field}:${s.direction}`).join(','), ); } if (filterModel) params.set('filter', JSON.stringify(filterModel)); const response = await fetch(`/api/employees?${params}`, { signal }); const { rows, totalNodeCount } = await response.json(); return { rows, totalNodeCount }; }, }), []); return ( <DataGrid columns={columns} multiSort serverSide={{ dataSource, pageSize: 100 }} /> ); } ``` :::tip Avoid `gridConfig.sortHandler` for server-side sort — it's bypassed when the multi-sort feature is loaded and only carries a single field. The `serverSide` feature is the supported path and works with both single- and multi-column sort. ::: ### React Query Integration If your app already uses [TanStack Query](https://tanstack.com/query/latest), one useful pairing is to back `ServerSidePlugin` with React Query's request cache. Each block the grid asks for becomes a query keyed by the request shape (`startNode`, `endNode`, `sortModel`, `filterModel`) — so scrolling back to a previously fetched range is served from cache instead of re-hitting the API, and you get retries, in-flight deduplication, and devtools for free. The pattern: keep `getRows` thin and delegate fetching to `queryClient.fetchQuery` with a stable key per block. ```tsx import '@toolbox-web/grid-react/features/server-side'; import '@toolbox-web/grid-react/features/multi-sort'; import { useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { DataGrid } from '@toolbox-web/grid-react'; import type { GetRowsParams, GetRowsResult, ServerSideDataSource, } from '@toolbox-web/grid/plugins/server-side'; async function fetchEmployeeBlock( params: GetRowsParams, ): Promise<GetRowsResult<Employee>> { const search = new URLSearchParams({ from: String(params.startNode), to: String(params.endNode), }); if (params.sortModel?.length) { search.set('sort', params.sortModel.map((s) => `${s.field}:${s.direction}`).join(',')); } if (params.filterModel) search.set('filter', JSON.stringify(params.filterModel)); const response = await fetch(`/api/employees?${search}`, { signal: params.signal }); return response.json(); } function ServerSideGrid() { const queryClient = useQueryClient(); const dataSource = useMemo<ServerSideDataSource<Employee>>(() => ({ // Each block becomes a cached query — React Query dedupes in-flight // requests, retries failed ones, and serves repeated scrolls from cache. getRows: (params) => queryClient.fetchQuery({ queryKey: [ 'employees', params.startNode, params.endNode, params.sortModel, params.filterModel, ], queryFn: ({ signal }) => fetchEmployeeBlock({ ...params, signal }), staleTime: 60_000, }), }), [queryClient]); return ( <DataGrid columns={columns} multiSort serverSide={{ dataSource, pageSize: 100 }} /> ); } ``` :::tip When the user mutates a row (commit, paste, etc.), invalidate the query to trigger a refresh: `queryClient.invalidateQueries({ queryKey: ['employees'] })`. The grid will re-request the affected blocks on next scroll. ::: ## Dynamic Row Updates ```tsx import { useState, useCallback } from 'react'; import { DataGrid } from '@toolbox-web/grid-react'; function EmployeeGrid() { const [employees, setEmployees] = useState<Employee[]>(initialData); const handleAdd = useCallback(() => { setEmployees(prev => [ ...prev, { id: Date.now(), name: 'New Employee', department: '', salary: 50000 }, ]); }, []); const handleDelete = useCallback(() => { // Remove first row as example setEmployees(prev => prev.slice(1)); }, []); return ( <div> <button onClick={handleAdd}>Add</button> <button onClick={handleDelete}>Remove First</button> <DataGrid rows={employees} columns={columns} /> </div> ); } ``` ### Smart Row Diffing When `getRowId` is configured in `gridConfig`, the adapter automatically diffs the previous and incoming `rows` array on every update. If only **values** changed — the row count and every row ID are the same — the adapter routes through `updateRows()` internally rather than replacing the whole array: - **Lighter render** — only the `VIRTUALIZATION` phase runs instead of the full `ROWS` phase. - **Selection preserved** — the `SelectionPlugin` only clears selection when `sourceRowCount` changes, so value-only updates keep any active selection and scroll position intact. Structural changes — rows added, removed, or **reordered**, or an ID appearing/disappearing — fall back to full replacement as before. The smart diff only runs when the row IDs match in the same order. ```tsx // ✅ Enable smart diffing via getRowId <DataGrid rows={employees} gridConfig={{ getRowId: (r) => r.id }} selection="row" /> // When setEmployees() is called with the same count and same IDs, // the adapter calls updateRows() instead of replacing grid.rows — selection stays. setEmployees(prev => prev.map(e => e.id === 42 ? { ...e, status: 'inactive' } : e), ); ``` Without `getRowId` the behaviour is unchanged — every update replaces `grid.rows`. ## Performance Tips ### Memoize Config and Columns ```tsx // ✅ Good: Memoized — stable reference const config = useMemo<GridConfig<Employee>>(() => ({ columns: [/* ... */], }), []); // ❌ Bad: Creates new config every render const config: GridConfig<Employee> = { columns: [/* ... */] }; ``` ### Use `React.memo` for Cell Components ```tsx const StatusBadge = React.memo(function StatusBadge({ status }: { status: string }) { return <span className={`badge badge--${status}`}>{status}</span>; }); ``` ## Troubleshooting ### Feature prop not working Ensure you imported the feature side-effect: ```tsx // ❌ This won't work <DataGrid selection="range" /> // ✅ Import the feature first import '@toolbox-web/grid-react/features/selection'; <DataGrid selection="range" /> ``` ### Config object recreated on every render Wrap in `useMemo`, or use feature props directly: ```tsx // ✅ Best: Feature props — no config object needed <DataGrid selection="row" /> // ✅ Also good: Stable config reference const config = useMemo(() => ({ features: { selection: true } }), []); ``` ### Grid not responding to row updates Create new arrays — the grid uses reference equality: ```tsx // ❌ Bad: Mutating existing array employees.push(newEmployee); setEmployees(employees); // ✅ Good: New array reference setEmployees([...employees, newEmployee]); ``` --- # Vue Components > Props, slots, emits and exposed members for every component exported by @toolbox-web/grid-vue. TypeDoc cannot parse Vue Single File Components, so the nine `.vue` components in `@toolbox-web/grid-vue` are documented here instead of in the generated API Reference. Everything else the package exports — composables, adapters, registries, and types — is generated into that reference (see **Vue → API Reference** in the sidebar). For task-oriented guidance, see [Vue Integration](https://toolboxjs.com/grid/vue/getting-started.md). ## TbwGrid Wraps `<tbw-grid>`, binds row data and column definitions, converts feature props into plugins, and forwards every grid event as a Vue emit. Generic parameter: `TRow = unknown` — the row shape. ### Props | Name | Type | Default | | ----------------- | ------------------------------- | ----------- | | `rows` | `TRow[]` | `[]` | | `columns` | `ColumnShorthand<TRow>[]` | `undefined` | | `columnDefaults` | `Partial<ColumnConfig<TRow>>` | `undefined` | | `gridConfig` | `GridConfig<TRow>` | `undefined` | | `fitMode` | `FitMode` | `undefined` | | `columnInference` | `ColumnInferenceMode` | `undefined` | | `sortable` | `boolean` | `undefined` | | `filterable` | `boolean` | `undefined` | | `selectable` | `boolean` | `undefined` | | `loading` | `boolean` | `undefined` | | `customStyles` | `string` | `undefined` | In addition, one **feature prop** per plugin is accepted. All of them are optional and default to `undefined`; passing one enables the matching plugin. The full set and its exact types live in [`AllFeatureProps`](https://toolboxjs.com/grid/vue/api/types/allfeatureprops.md): `selection`, `editing`, `clipboard`, `contextMenu`, `multiSort`, `filtering`, `reorderColumns`, `visibility`, `pinnedColumns`, `groupingColumns`, `columnVirtualization`, `rowDragDrop`, `groupingRows`, `pinnedRows`, `stickyRows`, `tree`, `masterDetail`, `responsive`, `undoRedo`, `export`, `print`, `pivot`, `serverSide`, `tooltip`. Each feature prop requires its side-effect import (for example `import '@toolbox-web/grid-vue/features/selection'`). See [Vue Integration](https://toolboxjs.com/grid/vue/getting-started.md) for the import list. ### Emits Every event in the core [`DataGridEventMap`](https://toolboxjs.com/grid/api-reference.md#events) is re-emitted under its native name, with the original `CustomEvent` as the payload — for example `@cell-click`, `@sort-change`, `@selection-change`, `@row-commit`, `@tbw-scroll`, `@render`. ### Slots | Name | Slot props | Purpose | | --------- | ---------- | ----------------------------------------------------------------------------- | | `default` | none | Child declarations: `TbwGridColumn`, `TbwGridDetailPanel`, tool panels, types. | ### Exposed members | Name | Type | | ------------- | ---------------------------------------- | | `element` | `Ref<DataGridElement<TRow> \| null>` | | `gridElement` | `Ref<DataGridElement<TRow> \| null>` — deprecated alias for `element` | | `forceLayout` | `() => void` | | `getConfig` | `() => Promise<GridConfig<TRow> \| null>` | | `ready` | `() => Promise<void> \| undefined` | ### Notes - `inheritAttrs` is `false`; unrecognised attributes are forwarded to the inner `<tbw-grid>` via `v-bind="$attrs"`. - When `getRowId` is configured and `rows` is replaced with a new array reference, the component diffs by value and calls `updateRows()` with source `'sync'` so unchanged rows are not marked dirty. - A teleport manager mounts renderers with the parent component's context, so `provide`/`inject`, Pinia, Vue Router, and i18n work inside cells, editors, detail panels, and tool panels. ## TbwGridColumn Declares one column and, optionally, its Vue-rendered cell, editor, and header content. Must be a child of `TbwGrid`. Generic parameters: `TRow = unknown`, `TValue = any`. Specify them (`<TbwGridColumn<Employee, string>>`) to get fully typed slot props. ### Props | Name | Type | Required | | ---------- | -------------------------------- | -------- | | `field` | `string` | yes | | `header` | `string` | no | | `type` | `ColumnType` | no | | `width` | `string \| number` | no | | `minWidth` | `string \| number` | no | | `order` | `number` | no | | `sortable` | `boolean` | no | | `resizable`| `boolean` | no | | `editable` | `boolean` | no | | `hidden` | `boolean` | no | | `lockVisible` | `boolean` | no | | `options` | [`ColumnOptions`](#columnoptions) | no | #### ColumnOptions `options` supplies the choice list for `select` / `typeahead` column types. Pass either bare values or `{ label, value }` pairs; the component serializes them into the `options` attribute the core column parser reads. Values and labels must not contain `,` or `:`. ```vue <TbwGridColumn field="role" type="select" :options="['admin', 'user']" /> <TbwGridColumn field="role" type="select" :options="[{ label: 'Administrator', value: 'admin' }, { label: 'User', value: 'user' }]" /> ``` The React adapter accepts the identical shape via `<GridColumn options={...} />`. ### Slots | Name | Slot props type | Purpose | | ------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `cell` | [`CellSlotProps<TRow, TValue>`](https://toolboxjs.com/grid/vue/api/types/cellslotprops.md) | Cell renderer. | | `editor` | [`EditorSlotProps<TRow, TValue>`](https://toolboxjs.com/grid/vue/api/types/editorslotprops.md) | Cell editor. Receives `commit`, `cancel`, `updateRow`, `onValueChange`. | | `header` | [`HeaderSlotProps<TRow>`](https://toolboxjs.com/grid/vue/api/types/headerslotprops.md) | Whole header cell. You own sort icons and filter buttons via `renderSortIcon` / `renderFilterButton`. Resize handles are still appended by the grid. | | `headerLabel` | [`HeaderLabelSlotProps<TRow>`](https://toolboxjs.com/grid/vue/api/types/headerlabelslotprops.md) | Label only. The grid keeps sort icons, filter buttons, and resize handles. | ## TbwGridType Registers a renderer and/or editor for every column whose `type` matches `name`. Place it inside `TbwGrid`. Generic parameters: `TRow = unknown`, `TValue = any`. ### Props | Name | Type | Required | | -------- | ------------------------------------------------ | -------- | | `name` | `string` | yes | | `params` | `Record<string, string \| number \| boolean>` | no | `params` entries become kebab-cased `data-*` attributes on the underlying `<tbw-grid-type>` element (`statusCode: 'pending'` → `data-status-code="pending"`). ### Slots | Name | Slot props type | Purpose | | -------- | ------------------------------------------------------------------------ | -------------------------------------------------- | | `cell` | [`CellSlotProps<TRow, TValue>`](https://toolboxjs.com/grid/vue/api/types/cellslotprops.md) | Type-level cell renderer. | | `editor` | [`EditorSlotProps<TRow, TValue>`](https://toolboxjs.com/grid/vue/api/types/editorslotprops.md) | Type-level editor. Requires the editing feature. | ## TbwGridDetailPanel Registers a detail-panel renderer for expandable rows. Requires the [Master-Detail plugin](https://toolboxjs.com/grid/plugins/master-detail.md). ### Props | Name | Type | Default | | ------------------- | ----------------------------- | --------- | | `showExpandColumn` | `boolean` | `true` | | `animation` | `'slide' \| 'fade' \| false` | `'slide'` | ### Slots | Name | Slot props type | Purpose | | --------- | ---------------------------------------------------------------------------- | --------------------- | | `default` | [`DetailPanelContext<TRow>`](https://toolboxjs.com/grid/vue/api/types/detailpanelcontext.md) | `{ row, rowIndex }`. | ## TbwGridResponsiveCard Registers a card renderer used by the [Responsive plugin](https://toolboxjs.com/grid/plugins/responsive.md) when the grid switches to card layout. ### Props | Name | Type | Default | | ---------------- | ------------------- | -------- | | `cardRowHeight` | `number \| 'auto'` | `'auto'` | ### Slots | Name | Slot props type | Purpose | | --------- | ---------------------------------------------------------------------------------- | --------------------- | | `default` | [`ResponsiveCardContext<TRow>`](https://toolboxjs.com/grid/vue/api/types/responsivecardcontext.md) | `{ row, rowIndex }`. | ## TbwGridToolPanel Registers a collapsible side panel in the grid shell. Requires the [Shell plugin](https://toolboxjs.com/grid/plugins/shell.md). ### Props | Name | Type | Required | Default | | ---------- | -------------------- | -------- | --------- | | `id` | `string` | yes | — | | `title` | `string` | no | `undefined` | | `label` | `string` | no | `undefined` | | `icon` | `string` | no | `undefined` | | `tooltip` | `string` | no | `undefined` | | `order` | `number` | no | `100` | `title` is the canonical name and matches the React and Angular adapters. `label` is a Vue-only alias kept for backward compatibility; when both are set, `title` wins. `order` controls the tool-button position in the shell rail (lower comes first). ### Slots | Name | Slot props type | Purpose | | --------- | --------------------------------------------------------------------- | ------------------- | | `default` | [`ToolPanelContext`](https://toolboxjs.com/grid/vue/api/types/toolpanelcontext.md) | `{ gridElement }`. | ## TbwGridHeaderContent Teleports reactive Vue content into the shell header. Must be a descendant of `TbwGrid` and requires the [Shell plugin](https://toolboxjs.com/grid/plugins/shell.md). ### Props | Name | Type | Default | | ------- | -------- | -------------------- | | `id` | `string` | auto-generated | | `order` | `number` | `100` | Changing `id` or `order` after mount unregisters and re-registers the content. ### Slots | Name | Slot props | Purpose | | --------- | ---------- | -------------------------------- | | `default` | none | Content placed in the header bar. | ## TbwGridToolbarContent Teleports reactive Vue content into the shell toolbar. Same props and slot as `TbwGridHeaderContent`. Use it when the toolbar content needs Vue state, props, or callbacks; use `TbwGridToolButtons` for static markup. ## TbwGridToolButtons Light-DOM passthrough that renders its children inside `<tbw-grid-tool-buttons>`. No props, no emits, and no context preservation — the markup is moved verbatim. ### Slots | Name | Slot props | Purpose | | --------- | ---------- | ---------------- | | `default` | none | Button markup. | --- # Vue Integration > Install and configure @toolbox-web/grid-vue — feature props, slot renderers, editors, composables, and event handling. The `@toolbox-web/grid-vue` package provides Vue 3 integration for the `<tbw-grid>` data grid component. :::note[Where to find feature docs] This page covers Vue-specific setup and APIs (feature props, composables, slot renderers). Core grid features and plugins — cell renderers, editors, events, master-detail, sorting, filtering, selection, etc. — are documented on the [Core](https://toolboxjs.com/grid/core.md) and [Plugins](https://toolboxjs.com/grid/plugins.md) pages, each with a Vue tab and runnable demos. ::: ## Compatibility | Vue version | Support level | | ----------- | ------------- | | 3.5+ | **Tested** — used in demos and CI | | 3.3 – 3.4 | Supported (minimum peer dependency) | | < 3.3 | Not supported | | Vue 2 | Not supported — adapter requires Vue 3 Composition API | ## Installation #### npm ```bash npm install @toolbox-web/grid @toolbox-web/grid-vue ``` #### yarn ```bash yarn add @toolbox-web/grid @toolbox-web/grid-vue ``` #### pnpm ```bash pnpm add @toolbox-web/grid @toolbox-web/grid-vue ``` #### bun ```bash bun add @toolbox-web/grid @toolbox-web/grid-vue ``` ## Setup No grid bootstrap step is required. Importing anything from `@toolbox-web/grid-vue` (the `TbwGrid` component, a composable, or any `features/*` entry) registers the `<tbw-grid>` custom element as a side effect — so you do **not** need a global `import '@toolbox-web/grid';` in `main.ts`. Import runtime values (the component, composables, providers) **and** the config types from `@toolbox-web/grid-vue`. The adapter's `GridConfig` / `ColumnConfig` widen `renderer` / `editor` to accept Vue components/slots — the base types in `@toolbox-web/grid` only accept `HTMLElement | string`, so importing them here breaks component renderers: ```typescript import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GridConfig, ColumnConfig } from '@toolbox-web/grid-vue'; ``` Reach into `@toolbox-web/grid` only for types the adapter doesn't re-export (event-detail types, enums, plugin option interfaces). Enable features with **co-located** side-effect imports — put them in the `.vue` component that renders the grid, not in `main.ts`: ```typescript // Inside the grid component's <script setup> // Only import the features you use - this keeps your bundle small! import '@toolbox-web/grid-vue/features/selection'; import '@toolbox-web/grid-vue/features/editing'; import '@toolbox-web/grid-vue/features/multi-sort'; import '@toolbox-web/grid-vue/features/filtering'; ``` **Important**: The one genuine global step is telling Vue to treat `tbw-*` tags as custom elements in your `vite.config.ts`: ```typescript // vite.config.ts import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { // Tell Vue that tbw-* tags are web components isCustomElement: (tag) => tag.startsWith('tbw-'), }, }, }), ], }); ``` ## Basic Usage The simplest way to use the grid is with the `TbwGrid` component and feature props: ```html <script setup lang="ts"> // Enable features you use import '@toolbox-web/grid-vue/features/selection'; import '@toolbox-web/grid-vue/features/editing'; import '@toolbox-web/grid-vue/features/multi-sort'; import '@toolbox-web/grid-vue/features/filtering'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { ColumnConfig } from '@toolbox-web/grid-vue'; import { ref } from 'vue'; interface Employee { id: number; name: string; department: string; salary: number; } const employees = ref<Employee[]>([ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Charlie', department: 'Sales', salary: 85000 }, ]); const columns: ColumnConfig<Employee>[] = [ { field: 'id', header: 'ID', type: 'number', width: 70 }, { field: 'name', header: 'Name', editable: true, sortable: true }, { field: 'department', header: 'Department', editable: true, sortable: true }, { field: 'salary', header: 'Salary', type: 'number', format: (v: number) => '$' + v.toLocaleString() }, ]; </script> <template> <TbwGrid :rows="employees" :columns="columns" selection="range" editing="dblclick" :multi-sort="true" :filtering="{ debounceMs: 200 }" style="height: 400px; display: block" /> </template> ``` ## Feature Prop Reference Each feature prop enables a specific plugin with simplified configuration: | Feature Prop | Feature Import | Description | |--------------|----------------|-------------| | `selection` | `features/selection` | Cell, row, or range selection | | `editing` | `features/editing` | Inline cell editing | | `multi-sort` | `features/multi-sort` | Multi-column sorting | | `filtering` | `features/filtering` | Column filtering | | `clipboard` | `features/clipboard` | Copy/paste support | | `context-menu` | `features/context-menu` | Right-click context menu | | `reorder-columns` | `features/reorder-columns` | Column drag-to-reorder | | `visibility` | `features/visibility` | Column visibility panel | | `pinned-columns` | `features/pinned-columns` | Sticky left/right columns | | `pinned-rows` | `features/pinned-rows` | Sticky top/bottom rows | | `grouping-columns` | `features/grouping-columns` | Multi-level column headers | | `grouping-rows` | `features/grouping-rows` | Row grouping | | `column-virtualization` | `features/column-virtualization` | Virtualize columns for wide grids | | `tree` | `features/tree` | Hierarchical tree view | | `master-detail` | `features/master-detail` | Expandable detail rows | | `responsive` | `features/responsive` | Card layout for narrow viewports | | `undo-redo` | `features/undo-redo` | Edit undo/redo | | `export` | `features/export` | CSV/Excel export | | `print` | `features/print` | Print support | | `pivot` | `features/pivot` | Pivot table functionality | | `server-side` | `features/server-side` | Server-side data loading | | `sticky-rows` | `features/sticky-rows` | Pin selected data rows below the header on scroll | | `row-drag-drop` | `features/row-drag-drop` | Row drag-to-reorder and cross-grid row transfer | | `tooltip` | `features/tooltip` | Popover tooltips for truncated cell / header text | | `row-drag-drop` | `features/row-drag-drop` | Row drag-and-drop (within and across grids) | | `tooltip` | `features/tooltip` | Cell / header tooltips | **Core Config Props (no feature import needed):** | Prop | Type | Description | |------|------|-------------| | `sortable` | `boolean` | Grid-wide sorting toggle (default: true) | | `filterable` | `boolean` | Grid-wide filtering toggle (default: true). Requires FilteringPlugin. | | `selectable` | `boolean` | Grid-wide selection toggle (default: true). Requires SelectionPlugin. | ## Header & Toolbar Content Inject Vue components into the grid's shell header (left zone) or toolbar (right zone) via `<TbwGridHeaderContent>` and `<TbwGridToolbarContent>`. These wrap the imperative `registerHeaderContent` / `registerToolbarContent` APIs using Vue's built-in `<Teleport>`, so slot content keeps full reactivity (props, refs, computed, slots, provide/inject). ```html <script setup lang="ts"> import { TbwGrid, TbwGridHeaderContent, TbwGridToolbarContent } from '@toolbox-web/grid-vue'; import { ref } from 'vue'; const year = ref(new Date().getFullYear()); </script> <template> <TbwGrid :rows="rows" :grid-config="config"> <TbwGridHeaderContent id="calendar-nav" :order="0"> <HeaderNav :year="year" @year-change="year = $event" /> </TbwGridHeaderContent> <TbwGridToolbarContent id="calendar-buttons" :order="0"> <ToolbarNav @prev="prev" @today="today" @next="next" /> </TbwGridToolbarContent> </TbwGrid> </template> ``` **Props:** `id` (optional — auto-generated if omitted), `order` (default `100`, lower = first). Slot content updates reactively without re-registering with the grid. ## Programmatic Grid Access Use the `useGrid` composable for programmatic control: ```html <script setup lang="ts"> import { TbwGrid, useGrid } from '@toolbox-web/grid-vue'; import { ref } from 'vue'; interface Employee { id: number; name: string; } const employees = ref<Employee[]>([/* ... */]); const { element, forceLayout } = useGrid(); function refreshGrid() { forceLayout(); } async function exportCsv() { const grid = element.value; if (!grid) return; const exportPlugin = grid.getPluginByName('export'); await exportPlugin?.exportToCsv({ filename: 'employees.csv' }); } </script> <template> <div> <button @click="refreshGrid">Refresh</button> <button @click="exportCsv">Export CSV</button> <TbwGrid :rows="employees" :columns="columns" /> </div> </template> ``` ## Feature-Scoped Composables Feature imports export **scoped composables** for type-safe programmatic access to plugin functionality: ```html <script setup lang="ts"> import '@toolbox-web/grid-vue/features/export'; import { useGridExport } from '@toolbox-web/grid-vue/features/export'; import { TbwGrid } from '@toolbox-web/grid-vue'; import { ref } from 'vue'; const employees = ref<Employee[]>([/* ... */]); const { exportToCsv, exportToExcel, isExporting } = useGridExport(); </script> <template> <div> <button @click="exportToCsv('employees.csv')" :disabled="isExporting()">Export CSV</button> <TbwGrid :rows="employees" :columns="columns" export /> </div> </template> ``` ### Available Feature Composables See the **API Reference > Composables** section for detailed method signatures, return types, and examples. All composables accept an optional `selector` parameter to target a specific grid via DOM query instead of Vue's provide/inject. Use when a component contains multiple grids: ```html <script setup> import { useGridSelection } from '@toolbox-web/grid-vue/features/selection'; // Target a specific grid by CSS selector const selection = useGridSelection('tbw-grid.primary'); </script> ``` | Composable | Import | Key Methods | |------------|--------|-------------| | `useGridSelection()` | `features/selection` | `selectAll`, `clearSelection`, `getSelection`, `getSelectedRows` | | `useGridFiltering()` | `features/filtering` | `setFilter`, `clearAllFilters`, `getFilters`, `getFilteredRowCount` | | `useGridExport()` | `features/export` | `exportToCsv`, `exportToExcel`, `exportToJson`, `isExporting` | | `useGridPrint()` | `features/print` | `print`, `isPrinting` | | `useGridUndoRedo()` | `features/undo-redo` | `undo`, `redo`, `canUndo`, `canRedo` | ## Overlay Editors (`useGridOverlay`) Custom Vue editors that open a popover, listbox, calendar, or other floating panel typically render that panel through `<Teleport>` so it can escape the cell's overflow clipping. Without help, the grid sees a click into the teleported panel as a click _outside_ the editor and commits-and-exits the row before the user can pick an option. The `useGridOverlay` composable bridges that gap by registering the panel with the grid as an external focus container — the Vue equivalent of Angular's `BaseOverlayEditor.initOverlay()` and React's `useGridOverlay`. Pair it with `aria-expanded` / `aria-controls` on the trigger so even editors that forget to call the composable get correct keyboard / pointer behaviour out of the box (the editing plugin honours those attributes as a generic fallback — see [issue #251](https://github.com/OysteinAmundsen/toolbox/issues/251)). ```html <script setup lang="ts"> import { ref, useId } from 'vue'; import { useGridOverlay, type GridEditorContext } from '@toolbox-web/grid-vue'; const props = defineProps<{ ctx: GridEditorContext<MyRow> }>(); const open = ref(false); const panel = ref<HTMLElement | null>(null); const listboxId = useId(); // Registers panel.value with the grid while open.value === true; unregisters // automatically on close or unmount. useGridOverlay(panel, { open }); </script> <template> <input role="combobox" :aria-expanded="open" :aria-controls="listboxId" :value="ctx.value ?? ''" @click="open = true" @keydown.escape="ctx.cancel()" /> <Teleport to="body" v-if="open"> <div ref="panel" :id="listboxId" role="listbox" class="my-listbox"> <!-- options that call ctx.commit(option) on click --> </div> </Teleport> </template> ``` `useGridOverlay(panelRef, options?)` resolves the owning `<tbw-grid>` in this order: 1. `options.gridElement` if explicitly passed (accepts a `MaybeRef<DataGridElement>`), 2. `panelRef.value.closest('tbw-grid')` for inline (non-teleported) panels, 3. The `GRID_ELEMENT_KEY` injection populated by `<TbwGrid>` / `<GridProvider>`. `options.open` accepts a `MaybeRef<boolean>` so reactive refs and plain booleans both work. The grid host is also exposed on `ColumnEditorContext.grid` for editors that need to call grid APIs directly without using the composable. ## Manual Plugin Instantiation For custom configurations or third-party plugins, instantiate manually using `markRaw()`: ```html <script setup lang="ts"> import { TbwGrid, type GridConfig } from '@toolbox-web/grid-vue'; import { SelectionPlugin, EditingPlugin, ClipboardPlugin } from '@toolbox-web/grid/all'; import { markRaw, ref } from 'vue'; const employees = ref<Employee[]>([/* ... */]); const config = markRaw<GridConfig<Employee>>({ columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, ], plugins: [ new SelectionPlugin({ mode: 'range', checkbox: true }), new EditingPlugin({ editOn: 'dblclick' }), new ClipboardPlugin({ includeHeaders: true }), ], }); </script> <template> <TbwGrid :rows="employees" :grid-config="config" /> </template> ``` > **Important**: Always use `markRaw()` for grid configs containing plugins. Vue's reactivity system can interfere with plugin class instances. Feature props and manual plugins can be mixed — the grid merges them. ## Custom Icons The grid supports two complementary ways to customize icons — see the [Theming Guide → Icon Customization](https://toolboxjs.com/grid/guides/theming.md#icon-customization) for both the CSS and JavaScript approaches: - **CSS variables** (`--tbw-icon-*`) — preferred for themes and static customization; no JavaScript needed. - **`gridConfig.icons`** — for dynamic icons, icon libraries, or `HTMLElement` instances; takes precedence over CSS. `GridIconProvider` is the Vue wrapper for the JS path — it injects `icons` into `gridConfig.icons` for every descendant `<TbwGrid>`: ```html <script setup lang="ts"> import { GridIconProvider, TbwGrid } from '@toolbox-web/grid-vue'; </script> <template> <GridIconProvider :icons="{ sortAsc: '↑', sortDesc: '↓' }"> <TbwGrid :rows="employees" :columns="columns" /> </GridIconProvider> </template> ``` ## Server-Side Data Loading For backends that own paging / sorting / filtering, use the `serverSide` feature prop. Your `getRows` handler receives the current `sortModel`, `filterModel`, and block range — return rows and the total count. See [`ServerSideDataSource`](https://toolboxjs.com/grid/plugins/server-side/interfaces/serversidedatasource.md), [`GetRowsParams`](https://toolboxjs.com/grid/plugins/server-side/interfaces/getrowsparams.md), and [`GetRowsResult`](https://toolboxjs.com/grid/plugins/server-side/interfaces/getrowsresult.md) for the full contract — including the `params.signal` `AbortSignal` you can forward to `fetch()` for explicit cancellation. ```html <script setup lang="ts"> import '@toolbox-web/grid-vue/features/server-side'; import '@toolbox-web/grid-vue/features/multi-sort'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side'; const dataSource: ServerSideDataSource<Employee> = { getRows: async ({ startNode, endNode, sortModel, filterModel, signal }) => { const params = new URLSearchParams({ from: String(startNode), to: String(endNode), }); if (sortModel?.length) { params.set( 'sort', sortModel.map((s) => `${s.field}:${s.direction}`).join(','), ); } if (filterModel) params.set('filter', JSON.stringify(filterModel)); const response = await fetch(`/api/employees?${params}`, { signal }); const { rows, totalNodeCount } = await response.json(); return { rows, totalNodeCount }; }, }; </script> <template> <TbwGrid :columns="columns" multi-sort :server-side="{ dataSource, pageSize: 100 }" /> </template> ``` :::tip Avoid `gridConfig.sortHandler` for server-side sort — it's bypassed when the multi-sort feature is loaded and only carries a single field. The `serverSide` feature is the supported path and works with both single- and multi-column sort. ::: ### VueQuery (TanStack Query) Integration If your app already uses [TanStack Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview), one useful pairing is to back `ServerSidePlugin` with Vue Query's request cache. Each block the grid asks for becomes a query keyed by the request shape (`startNode`, `endNode`, `sortModel`, `filterModel`) — so scrolling back to a previously fetched range is served from cache instead of re-hitting the API, and you get retries, in-flight deduplication, and devtools for free. The pattern: keep `getRows` thin and delegate fetching to `queryClient.fetchQuery` with a stable key per block. ```html <script setup lang="ts"> import '@toolbox-web/grid-vue/features/server-side'; import '@toolbox-web/grid-vue/features/multi-sort'; import { TbwGrid } from '@toolbox-web/grid-vue'; import type { GetRowsParams, GetRowsResult, ServerSideDataSource, } from '@toolbox-web/grid/plugins/server-side'; import { useQueryClient } from '@tanstack/vue-query'; async function fetchEmployeeBlock( params: GetRowsParams, ): Promise<GetRowsResult<Employee>> { const search = new URLSearchParams({ from: String(params.startNode), to: String(params.endNode), }); if (params.sortModel?.length) { search.set('sort', params.sortModel.map((s) => `${s.field}:${s.direction}`).join(',')); } if (params.filterModel) search.set('filter', JSON.stringify(params.filterModel)); const response = await fetch(`/api/employees?${search}`, { signal: params.signal }); return response.json(); } const queryClient = useQueryClient(); const dataSource: ServerSideDataSource<Employee> = { // Each block becomes a cached query — Vue Query dedupes in-flight // requests, retries failed ones, and serves repeated scrolls from cache. getRows: (params) => queryClient.fetchQuery({ queryKey: [ 'employees', params.startNode, params.endNode, params.sortModel, params.filterModel, ], queryFn: ({ signal }) => fetchEmployeeBlock({ ...params, signal }), staleTime: 60_000, }), }; </script> <template> <TbwGrid :columns="columns" multi-sort :server-side="{ dataSource, pageSize: 100 }" /> </template> ``` :::tip When the user mutates a row (commit, paste, etc.), invalidate the query to trigger a refresh: `queryClient.invalidateQueries({ queryKey: ['employees'] })`. The grid will re-request the affected blocks on next scroll. ::: ## Dynamic Row Updates ```html <script setup lang="ts"> import { TbwGrid } from '@toolbox-web/grid-vue'; import { ref } from 'vue'; const employees = ref<Employee[]>([ { id: 1, name: 'Alice', department: 'Engineering' }, { id: 2, name: 'Bob', department: 'Design' }, ]); let nextId = 3; function handleAdd() { employees.value = [ ...employees.value, { id: nextId++, name: 'New Employee', department: 'TBD' }, ]; } function removeFirst() { employees.value = employees.value.slice(1); } </script> <template> <div> <button @click="handleAdd">Add</button> <button @click="removeFirst">Remove First</button> <TbwGrid :rows="employees" :columns="columns" /> </div> </template> ``` ### Smart Row Diffing When `getRowId` is configured in `:grid-config`, the adapter automatically diffs the previous and incoming `:rows` binding on every update. If only **values** changed — the row count and every row ID are the same — the adapter routes through `updateRows()` internally rather than replacing the whole array: - **Lighter render** — only the `VIRTUALIZATION` phase runs instead of the full `ROWS` phase. - **Selection preserved** — the `SelectionPlugin` only clears selection when `sourceRowCount` changes, so value-only updates keep any active selection and scroll position intact. Structural changes — rows added, removed, or **reordered**, or an ID appearing/disappearing — fall back to full replacement as before. The smart diff only runs when the row IDs match in the same order. ```html <TbwGrid :rows="employees" :grid-config="{ getRowId: (r) => String(r.id) }" selection="row" /> ``` When `employees.value` is replaced with the same count and same IDs, the adapter calls `updateRows()` instead — selection stays. :::tip[Mutable reactive updates] If you mutate row properties directly (e.g. `employees.value[0].status = 'inactive'`), Vue's deep watcher still fires the adapter, which falls through to a full `grid.rows` assignment to trigger a re-render. The diff path only runs when the array reference itself changes. ::: ## Performance Tips ### Use `markRaw` for Config Objects ```typescript import { markRaw } from 'vue'; // ✅ Prevents Vue from making config deeply reactive const config = markRaw({ columns: [...], features: { selection: true } }); ``` ### Use `shallowRef` for Large Datasets ```typescript import { shallowRef } from 'vue'; // ✅ Only tracks reference changes, not deep mutations const employees = shallowRef<Employee[]>([]); employees.value = [...employees.value, newEmployee]; ``` ### Memoize Column Configurations ```typescript import { computed, markRaw } from 'vue'; // ✅ Only recreated when dependencies change const columns = computed(() => markRaw([ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, ])); ``` ## Troubleshooting ### Feature prop not working Ensure you imported the feature side-effect in your `main.ts`: ```typescript import '@toolbox-web/grid-vue/features/selection'; ``` ### `tbw-grid` not recognized as a component Add the `isCustomElement` config to your Vite setup: ```typescript // vite.config.ts vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('tbw-'), }, }, }) ``` ### Row updates not reflected The grid detects updates via reference equality. Always assign a new array: ```typescript // ✅ New reference — grid re-renders employees.value = [...employees.value, newRow]; // ❌ Same reference — grid won't detect change employees.value.push(newRow); ``` --- ## API Reference The complete generated TypeDoc reference is not inlined above. Every symbol has a plain-markdown companion — fetch any link below (or append `.md` to its page URL). ### Core - [A11yconfig](https://toolboxjs.com/grid/api/core/interfaces/a11yconfig.md) - [A11ymessages](https://toolboxjs.com/grid/api/core/interfaces/a11ymessages.md) - [Aggregatorref](https://toolboxjs.com/grid/api/core/types/aggregatorref.md) - [Animationconfig](https://toolboxjs.com/grid/api/core/interfaces/animationconfig.md) - [Animationmode](https://toolboxjs.com/grid/api/core/types/animationmode.md) - [Animationstyle](https://toolboxjs.com/grid/api/core/types/animationstyle.md) - [Applycolumndefaults](https://toolboxjs.com/grid/api/core/functions/applycolumndefaults.md) - [Basecolumnconfig](https://toolboxjs.com/grid/api/core/interfaces/basecolumnconfig.md) - [Builtinsort](https://toolboxjs.com/grid/api/core/functions/builtinsort.md) - [Cellactivatedetail](https://toolboxjs.com/grid/api/core/interfaces/cellactivatedetail.md) - [Cellactivatetrigger](https://toolboxjs.com/grid/api/core/types/cellactivatetrigger.md) - [Cellchangedetail](https://toolboxjs.com/grid/api/core/interfaces/cellchangedetail.md) - [Cellclickdetail](https://toolboxjs.com/grid/api/core/interfaces/cellclickdetail.md) - [Cellclickevent](https://toolboxjs.com/grid/api/core/interfaces/cellclickevent.md) - [Cellcoords](https://toolboxjs.com/grid/api/core/interfaces/cellcoords.md) - [Cellrendercontext](https://toolboxjs.com/grid/api/core/interfaces/cellrendercontext.md) - [Columnconfig](https://toolboxjs.com/grid/api/core/interfaces/columnconfig.md) - [Columnconfigmap](https://toolboxjs.com/grid/api/core/types/columnconfigmap.md) - [Columneditorcontext](https://toolboxjs.com/grid/api/core/interfaces/columneditorcontext.md) - [Columneditorspec](https://toolboxjs.com/grid/api/core/types/columneditorspec.md) - [Columnfieldkey](https://toolboxjs.com/grid/api/core/types/columnfieldkey.md) - [Columninferencemode](https://toolboxjs.com/grid/api/core/types/columninferencemode.md) - [Columnresizedetail](https://toolboxjs.com/grid/api/core/interfaces/columnresizedetail.md) - [Columnresizeresetdetail](https://toolboxjs.com/grid/api/core/interfaces/columnresizeresetdetail.md) - [Columnshorthand](https://toolboxjs.com/grid/api/core/types/columnshorthand.md) - [Columnsortstate](https://toolboxjs.com/grid/api/core/interfaces/columnsortstate.md) - [Columnstate](https://toolboxjs.com/grid/api/core/interfaces/columnstate.md) - [Columntype](https://toolboxjs.com/grid/api/core/types/columntype.md) - [Columnviewrenderer](https://toolboxjs.com/grid/api/core/types/columnviewrenderer.md) - [Core](https://toolboxjs.com/grid/api/core.md) - [Core_consumed_adapter_methods](https://toolboxjs.com/grid/api/core/variables/core_consumed_adapter_methods.md) - [Creategrid](https://toolboxjs.com/grid/api/core/functions/creategrid.md) - [Datachangedetail](https://toolboxjs.com/grid/api/core/interfaces/datachangedetail.md) - [Datagridcustomevent](https://toolboxjs.com/grid/api/core/types/datagridcustomevent.md) - [Datagridelement](https://toolboxjs.com/grid/api/core/classes/datagridelement.md) - [Datagrideventmap](https://toolboxjs.com/grid/api/core/interfaces/datagrideventmap.md) - [Default_a11y_messages](https://toolboxjs.com/grid/api/core/variables/default_a11y_messages.md) - [Default_animation_config](https://toolboxjs.com/grid/api/core/variables/default_animation_config.md) - [Default_grid_icons](https://toolboxjs.com/grid/api/core/variables/default_grid_icons.md) - [Defaultcomparator](https://toolboxjs.com/grid/api/core/functions/defaultcomparator.md) - [Emptycontext](https://toolboxjs.com/grid/api/core/interfaces/emptycontext.md) - [Emptyoverlay](https://toolboxjs.com/grid/api/core/types/emptyoverlay.md) - [Emptyrenderer](https://toolboxjs.com/grid/api/core/types/emptyrenderer.md) - [Expandcollapseanimation](https://toolboxjs.com/grid/api/core/types/expandcollapseanimation.md) - [Featureconfig](https://toolboxjs.com/grid/api/core/interfaces/featureconfig.md) - [Fitmode](https://toolboxjs.com/grid/api/core/types/fitmode.md) - [Getbypath](https://toolboxjs.com/grid/api/core/functions/getbypath.md) - [Gridcolumnstate](https://toolboxjs.com/grid/api/core/interfaces/gridcolumnstate.md) - [Gridconfig](https://toolboxjs.com/grid/api/core/interfaces/gridconfig.md) - [Gridicons](https://toolboxjs.com/grid/api/core/interfaces/gridicons.md) - [Gridlocale](https://toolboxjs.com/grid/api/core/types/gridlocale.md) - [Hascolumnshorthands](https://toolboxjs.com/grid/api/core/functions/hascolumnshorthands.md) - [Headercellcontext](https://toolboxjs.com/grid/api/core/interfaces/headercellcontext.md) - [Headerclickevent](https://toolboxjs.com/grid/api/core/interfaces/headerclickevent.md) - [Headerlabelcontext](https://toolboxjs.com/grid/api/core/interfaces/headerlabelcontext.md) - [Headerlabelrenderer](https://toolboxjs.com/grid/api/core/types/headerlabelrenderer.md) - [Headerrenderer](https://toolboxjs.com/grid/api/core/types/headerrenderer.md) - [Iconvalue](https://toolboxjs.com/grid/api/core/types/iconvalue.md) - [Inferredcolumnresult](https://toolboxjs.com/grid/api/core/interfaces/inferredcolumnresult.md) - [Invalidateaccessorcache](https://toolboxjs.com/grid/api/core/functions/invalidateaccessorcache.md) - [Loadingcontext](https://toolboxjs.com/grid/api/core/interfaces/loadingcontext.md) - [Loadingrenderer](https://toolboxjs.com/grid/api/core/types/loadingrenderer.md) - [Loadingsize](https://toolboxjs.com/grid/api/core/types/loadingsize.md) - [Nestedpaths](https://toolboxjs.com/grid/api/core/types/nestedpaths.md) - [Normalizecolumns](https://toolboxjs.com/grid/api/core/functions/normalizecolumns.md) - [Parsecolumnshorthand](https://toolboxjs.com/grid/api/core/functions/parsecolumnshorthand.md) - [Parsefieldpath](https://toolboxjs.com/grid/api/core/functions/parsefieldpath.md) - [Primitivecolumntype](https://toolboxjs.com/grid/api/core/types/primitivecolumntype.md) - [Publicgrid](https://toolboxjs.com/grid/api/core/interfaces/publicgrid.md) - [Querygrid](https://toolboxjs.com/grid/api/core/functions/querygrid.md) - [Readcellfield](https://toolboxjs.com/grid/api/core/functions/readcellfield.md) - [Renderdetail](https://toolboxjs.com/grid/api/core/interfaces/renderdetail.md) - [Resolvecellvalue](https://toolboxjs.com/grid/api/core/functions/resolvecellvalue.md) - [Rowanimationtype](https://toolboxjs.com/grid/api/core/types/rowanimationtype.md) - [Rowclickdetail](https://toolboxjs.com/grid/api/core/interfaces/rowclickdetail.md) - [Rowclickevent](https://toolboxjs.com/grid/api/core/interfaces/rowclickevent.md) - [Rowgrouprenderconfig](https://toolboxjs.com/grid/api/core/interfaces/rowgrouprenderconfig.md) - [Rowtransaction](https://toolboxjs.com/grid/api/core/interfaces/rowtransaction.md) - [Rowupdate](https://toolboxjs.com/grid/api/core/interfaces/rowupdate.md) - [Scrollevent](https://toolboxjs.com/grid/api/core/interfaces/scrollevent.md) - [Scrolltorowoptions](https://toolboxjs.com/grid/api/core/interfaces/scrolltorowoptions.md) - [Setbypath](https://toolboxjs.com/grid/api/core/functions/setbypath.md) - [Sortchangedetail](https://toolboxjs.com/grid/api/core/interfaces/sortchangedetail.md) - [Sorthandler](https://toolboxjs.com/grid/api/core/types/sorthandler.md) - [Sortstate](https://toolboxjs.com/grid/api/core/interfaces/sortstate.md) - [Tbwscrolldetail](https://toolboxjs.com/grid/api/core/interfaces/tbwscrolldetail.md) - [Transactionresult](https://toolboxjs.com/grid/api/core/interfaces/transactionresult.md) - [Translate](https://toolboxjs.com/grid/api/core/types/translate.md) - [Typedefault](https://toolboxjs.com/grid/api/core/interfaces/typedefault.md) - [Updatesource](https://toolboxjs.com/grid/api/core/types/updatesource.md) - [Writecellfield](https://toolboxjs.com/grid/api/core/functions/writecellfield.md) ### Plugins - [Aggfunc](https://toolboxjs.com/grid/plugins/pivot/types/aggfunc.md) - [Aggregationrowconfig](https://toolboxjs.com/grid/plugins/pinned-rows/interfaces/aggregationrowconfig.md) - [Aggregationslot](https://toolboxjs.com/grid/plugins/pinned-rows/types/aggregationslot.md) - [Aggregatorconfig](https://toolboxjs.com/grid/plugins/pinned-rows/interfaces/aggregatorconfig.md) - [Aggregatordefinition](https://toolboxjs.com/grid/plugins/pinned-rows/types/aggregatordefinition.md) - [Aggregatorformatter](https://toolboxjs.com/grid/plugins/pinned-rows/types/aggregatorformatter.md) - [Aggregatormap](https://toolboxjs.com/grid/plugins/grouping-rows/types/aggregatormap.md) - [Baselinescaptureddetail](https://toolboxjs.com/grid/plugins/editing/interfaces/baselinescaptureddetail.md) - [Beforeeditclosedetail](https://toolboxjs.com/grid/plugins/editing/interfaces/beforeeditclosedetail.md) - [Blank_filter_value](https://toolboxjs.com/grid/plugins/filtering/variables/blank_filter_value.md) - [Blankmode](https://toolboxjs.com/grid/plugins/filtering/types/blankmode.md) - [Breakpointconfig](https://toolboxjs.com/grid/plugins/responsive/interfaces/breakpointconfig.md) - [Cellcanceldetail](https://toolboxjs.com/grid/plugins/editing/interfaces/cellcanceldetail.md) - [Cellcommitdetail](https://toolboxjs.com/grid/plugins/editing/interfaces/cellcommitdetail.md) - [Cellrange](https://toolboxjs.com/grid/plugins/selection/interfaces/cellrange.md) - [Changedrowsresetdetail](https://toolboxjs.com/grid/plugins/editing/interfaces/changedrowsresetdetail.md) - [Clipboardconfig](https://toolboxjs.com/grid/plugins/clipboard/interfaces/clipboardconfig.md) - [Clipboardplugin](https://toolboxjs.com/grid/plugins/clipboard/classes/clipboardplugin.md) - [Columngroup](https://toolboxjs.com/grid/plugins/grouping-columns/interfaces/columngroup.md) - [Columngroupdefinition](https://toolboxjs.com/grid/plugins/grouping-columns/interfaces/columngroupdefinition.md) - [Columngroupinfo](https://toolboxjs.com/grid/plugins/visibility/interfaces/columngroupinfo.md) - [Columnmovedetail](https://toolboxjs.com/grid/plugins/reorder-columns/interfaces/columnmovedetail.md) - [Columnpasteguard](https://toolboxjs.com/grid/plugins/clipboard/types/columnpasteguard.md) - [Columnreorderrequestdetail](https://toolboxjs.com/grid/plugins/visibility/interfaces/columnreorderrequestdetail.md) - [Columnvirtualizationconfig](https://toolboxjs.com/grid/plugins/column-virtualization/interfaces/columnvirtualizationconfig.md) - [Columnvirtualizationplugin](https://toolboxjs.com/grid/plugins/column-virtualization/classes/columnvirtualizationplugin.md) - [Columnvisibilitydetail](https://toolboxjs.com/grid/plugins/visibility/interfaces/columnvisibilitydetail.md) - [Compoundeditaction](https://toolboxjs.com/grid/plugins/undo-redo/interfaces/compoundeditaction.md) - [Contextmenuconfig](https://toolboxjs.com/grid/plugins/context-menu/interfaces/contextmenuconfig.md) - [Contextmenuitem](https://toolboxjs.com/grid/plugins/context-menu/interfaces/contextmenuitem.md) - [Contextmenuopendetail](https://toolboxjs.com/grid/plugins/context-menu/interfaces/contextmenuopendetail.md) - [Contextmenuparams](https://toolboxjs.com/grid/plugins/context-menu/interfaces/contextmenuparams.md) - [Contextmenuplugin](https://toolboxjs.com/grid/plugins/context-menu/classes/contextmenuplugin.md) - [Copydetail](https://toolboxjs.com/grid/plugins/clipboard/interfaces/copydetail.md) - [Copyoptions](https://toolboxjs.com/grid/plugins/clipboard/interfaces/copyoptions.md) - [Csvoptions](https://toolboxjs.com/grid/plugins/export/interfaces/csvoptions.md) - [Customaggfunc](https://toolboxjs.com/grid/plugins/pivot/types/customaggfunc.md) - [Datarequestmodel](https://toolboxjs.com/grid/plugins/server-side/interfaces/datarequestmodel.md) - [Datarowmodelitem](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/datarowmodelitem.md) - [Datasourcechildrendetail](https://toolboxjs.com/grid/plugins/server-side/interfaces/datasourcechildrendetail.md) - [Datasourcedatadetail](https://toolboxjs.com/grid/plugins/server-side/interfaces/datasourcedatadetail.md) - [Datasourceerrordetail](https://toolboxjs.com/grid/plugins/server-side/interfaces/datasourceerrordetail.md) - [Datasourceloadingdetail](https://toolboxjs.com/grid/plugins/server-side/interfaces/datasourceloadingdetail.md) - [Dateeditorparams](https://toolboxjs.com/grid/plugins/editing/interfaces/dateeditorparams.md) - [Defaulteditorfor](https://toolboxjs.com/grid/plugins/editing/functions/defaulteditorfor.md) - [Defaultexpandedvalue](https://toolboxjs.com/grid/plugins/grouping-rows/types/defaultexpandedvalue.md) - [Defaultpastehandler](https://toolboxjs.com/grid/plugins/clipboard/functions/defaultpastehandler.md) - [Detailexpanddetail](https://toolboxjs.com/grid/plugins/master-detail/interfaces/detailexpanddetail.md) - [Dirtychangedetail](https://toolboxjs.com/grid/plugins/editing/interfaces/dirtychangedetail.md) - [Dirtyrowentry](https://toolboxjs.com/grid/plugins/editing/interfaces/dirtyrowentry.md) - [Editaction](https://toolboxjs.com/grid/plugins/undo-redo/interfaces/editaction.md) - [Editclosedetail](https://toolboxjs.com/grid/plugins/editing/interfaces/editclosedetail.md) - [Editingconfig](https://toolboxjs.com/grid/plugins/editing/interfaces/editingconfig.md) - [Editingplugin](https://toolboxjs.com/grid/plugins/editing/classes/editingplugin.md) - [Editopendetail](https://toolboxjs.com/grid/plugins/editing/interfaces/editopendetail.md) - [Editorparams](https://toolboxjs.com/grid/plugins/editing/types/editorparams.md) - [Emitpasterejected](https://toolboxjs.com/grid/plugins/clipboard/functions/emitpasterejected.md) - [Excelborder](https://toolboxjs.com/grid/plugins/export/interfaces/excelborder.md) - [Excelcellstyle](https://toolboxjs.com/grid/plugins/export/interfaces/excelcellstyle.md) - [Excelstyleconfig](https://toolboxjs.com/grid/plugins/export/interfaces/excelstyleconfig.md) - [Exportcompletedetail](https://toolboxjs.com/grid/plugins/export/interfaces/exportcompletedetail.md) - [Exportconfig](https://toolboxjs.com/grid/plugins/export/interfaces/exportconfig.md) - [Exportformat](https://toolboxjs.com/grid/plugins/export/types/exportformat.md) - [Exportmode](https://toolboxjs.com/grid/plugins/export/types/exportmode.md) - [Exportparams](https://toolboxjs.com/grid/plugins/export/interfaces/exportparams.md) - [Exportplugin](https://toolboxjs.com/grid/plugins/export/classes/exportplugin.md) - [Fetchchildrenquery](https://toolboxjs.com/grid/plugins/server-side/interfaces/fetchchildrenquery.md) - [Filterchangedetail](https://toolboxjs.com/grid/plugins/filtering/interfaces/filterchangedetail.md) - [Filterconfig](https://toolboxjs.com/grid/plugins/filtering/interfaces/filterconfig.md) - [Filteredcountpanel](https://toolboxjs.com/grid/plugins/pinned-rows/functions/filteredcountpanel.md) - [Filterhandler](https://toolboxjs.com/grid/plugins/filtering/types/filterhandler.md) - [Filteringplugin](https://toolboxjs.com/grid/plugins/filtering/classes/filteringplugin.md) - [Filtermodel](https://toolboxjs.com/grid/plugins/filtering/interfaces/filtermodel.md) - [Filteroperator](https://toolboxjs.com/grid/plugins/filtering/types/filteroperator.md) - [Filterpanelparams](https://toolboxjs.com/grid/plugins/filtering/interfaces/filterpanelparams.md) - [Filterpanelrenderer](https://toolboxjs.com/grid/plugins/filtering/types/filterpanelrenderer.md) - [Filterparams](https://toolboxjs.com/grid/plugins/filtering/interfaces/filterparams.md) - [Filtertype](https://toolboxjs.com/grid/plugins/filtering/types/filtertype.md) - [Filtervalueshandler](https://toolboxjs.com/grid/plugins/filtering/types/filtervalueshandler.md) - [Flattenedtreerow](https://toolboxjs.com/grid/plugins/tree/interfaces/flattenedtreerow.md) - [Formatcsvparams](https://toolboxjs.com/grid/plugins/export/types/formatcsvparams.md) - [Formatexcelparams](https://toolboxjs.com/grid/plugins/export/types/formatexcelparams.md) - [Getchildrowsparams](https://toolboxjs.com/grid/plugins/server-side/interfaces/getchildrowsparams.md) - [Getchildrowsresult](https://toolboxjs.com/grid/plugins/server-side/interfaces/getchildrowsresult.md) - [Getrowsparams](https://toolboxjs.com/grid/plugins/server-side/interfaces/getrowsparams.md) - [Getrowsresult](https://toolboxjs.com/grid/plugins/server-side/interfaces/getrowsresult.md) - [Groupcollapsedetail](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/groupcollapsedetail.md) - [Groupdefinition](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/groupdefinition.md) - [Groupexpanddetail](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/groupexpanddetail.md) - [Groupheaderrenderparams](https://toolboxjs.com/grid/plugins/grouping-columns/interfaces/groupheaderrenderparams.md) - [Groupingcolumnsconfig](https://toolboxjs.com/grid/plugins/grouping-columns/interfaces/groupingcolumnsconfig.md) - [Groupingcolumnsplugin](https://toolboxjs.com/grid/plugins/grouping-columns/classes/groupingcolumnsplugin.md) - [Groupingrowsconfig](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/groupingrowsconfig.md) - [Groupingrowsplugin](https://toolboxjs.com/grid/plugins/grouping-rows/classes/groupingrowsplugin.md) - [Grouprowmodelitem](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/grouprowmodelitem.md) - [Grouprowrenderparams](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/grouprowrenderparams.md) - [Groupstate](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/groupstate.md) - [Grouptoggledetail](https://toolboxjs.com/grid/plugins/grouping-rows/interfaces/grouptoggledetail.md) - [Headercontentdefinition](https://toolboxjs.com/grid/plugins/shell/interfaces/headercontentdefinition.md) - [Headercontextmenuitem](https://toolboxjs.com/grid/plugins/context-menu/interfaces/headercontextmenuitem.md) - [Hiddencolumnconfig](https://toolboxjs.com/grid/plugins/responsive/types/hiddencolumnconfig.md) - [Masterdetailconfig](https://toolboxjs.com/grid/plugins/master-detail/interfaces/masterdetailconfig.md) - [Masterdetailplugin](https://toolboxjs.com/grid/plugins/master-detail/classes/masterdetailplugin.md) - [Multisortchangedetail](https://toolboxjs.com/grid/plugins/multi-sort/interfaces/multisortchangedetail.md) - [Multisortconfig](https://toolboxjs.com/grid/plugins/multi-sort/interfaces/multisortconfig.md) - [Multisortplugin](https://toolboxjs.com/grid/plugins/multi-sort/classes/multisortplugin.md) - [Numbereditorparams](https://toolboxjs.com/grid/plugins/editing/interfaces/numbereditorparams.md) - [Opentoolpaneloptions](https://toolboxjs.com/grid/plugins/shell/interfaces/opentoolpaneloptions.md) - [Panelrender](https://toolboxjs.com/grid/plugins/pinned-rows/types/panelrender.md) - [Panelslot](https://toolboxjs.com/grid/plugins/pinned-rows/interfaces/panelslot.md) - [Panelzone](https://toolboxjs.com/grid/plugins/pinned-rows/types/panelzone.md) - [Pastecellcontext](https://toolboxjs.com/grid/plugins/clipboard/interfaces/pastecellcontext.md) - [Pastedetail](https://toolboxjs.com/grid/plugins/clipboard/interfaces/pastedetail.md) - [Pastehandler](https://toolboxjs.com/grid/plugins/clipboard/types/pastehandler.md) - [Pasterejectedcell](https://toolboxjs.com/grid/plugins/clipboard/interfaces/pasterejectedcell.md) - [Pasterejecteddetail](https://toolboxjs.com/grid/plugins/clipboard/interfaces/pasterejecteddetail.md) - [Pasterejectionreason](https://toolboxjs.com/grid/plugins/clipboard/types/pasterejectionreason.md) - [Pasteresolution](https://toolboxjs.com/grid/plugins/clipboard/types/pasteresolution.md) - [Pastetarget](https://toolboxjs.com/grid/plugins/clipboard/interfaces/pastetarget.md) - [Pinnedcolumnsconfig](https://toolboxjs.com/grid/plugins/pinned-columns/interfaces/pinnedcolumnsconfig.md) - [Pinnedcolumnsplugin](https://toolboxjs.com/grid/plugins/pinned-columns/classes/pinnedcolumnsplugin.md) - [Pinnedposition](https://toolboxjs.com/grid/plugins/pinned-columns/types/pinnedposition.md) - [Pinnedrowsconfig](https://toolboxjs.com/grid/plugins/pinned-rows/interfaces/pinnedrowsconfig.md) - [Pinnedrowscontext](https://toolboxjs.com/grid/plugins/pinned-rows/interfaces/pinnedrowscontext.md) - [Pinnedrowslot](https://toolboxjs.com/grid/plugins/pinned-rows/types/pinnedrowslot.md) - [Pinnedrowsplugin](https://toolboxjs.com/grid/plugins/pinned-rows/classes/pinnedrowsplugin.md) - [Pinnedrowsposition](https://toolboxjs.com/grid/plugins/pinned-rows/types/pinnedrowsposition.md) - [Pivotconfig](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotconfig.md) - [Pivotconfigchangedetail](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotconfigchangedetail.md) - [Pivotdatarow](https://toolboxjs.com/grid/plugins/pivot/types/pivotdatarow.md) - [Pivotdefaultexpandedvalue](https://toolboxjs.com/grid/plugins/pivot/types/pivotdefaultexpandedvalue.md) - [Pivotplugin](https://toolboxjs.com/grid/plugins/pivot/classes/pivotplugin.md) - [Pivotresult](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotresult.md) - [Pivotrow](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotrow.md) - [Pivotsortconfig](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotsortconfig.md) - [Pivotsortdir](https://toolboxjs.com/grid/plugins/pivot/types/pivotsortdir.md) - [Pivotstatechangedetail](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotstatechangedetail.md) - [Pivottoggledetail](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivottoggledetail.md) - [Pivotvaluedisplaymode](https://toolboxjs.com/grid/plugins/pivot/types/pivotvaluedisplaymode.md) - [Pivotvaluefield](https://toolboxjs.com/grid/plugins/pivot/interfaces/pivotvaluefield.md) - [Printcompletedetail](https://toolboxjs.com/grid/plugins/print/interfaces/printcompletedetail.md) - [Printconfig](https://toolboxjs.com/grid/plugins/print/interfaces/printconfig.md) - [Printgridisolated](https://toolboxjs.com/grid/plugins/print/functions/printgridisolated.md) - [Printisolatedoptions](https://toolboxjs.com/grid/plugins/print/interfaces/printisolatedoptions.md) - [Printorientation](https://toolboxjs.com/grid/plugins/print/types/printorientation.md) - [Printparams](https://toolboxjs.com/grid/plugins/print/interfaces/printparams.md) - [Printplugin](https://toolboxjs.com/grid/plugins/print/classes/printplugin.md) - [Printstartdetail](https://toolboxjs.com/grid/plugins/print/interfaces/printstartdetail.md) - [Renderrow](https://toolboxjs.com/grid/plugins/grouping-rows/types/renderrow.md) - [Reorderanimation](https://toolboxjs.com/grid/plugins/reorder-columns/types/reorderanimation.md) - [Reorderconfig](https://toolboxjs.com/grid/plugins/reorder-columns/interfaces/reorderconfig.md) - [Reorderplugin](https://toolboxjs.com/grid/plugins/reorder-columns/classes/reorderplugin.md) - [Resolvecolumnpaste](https://toolboxjs.com/grid/plugins/clipboard/functions/resolvecolumnpaste.md) - [Responsiveanimation](https://toolboxjs.com/grid/plugins/responsive/types/responsiveanimation.md) - [Responsivechangedetail](https://toolboxjs.com/grid/plugins/responsive/interfaces/responsivechangedetail.md) - [Responsiveplugin](https://toolboxjs.com/grid/plugins/responsive/classes/responsiveplugin.md) - [Responsivepluginconfig](https://toolboxjs.com/grid/plugins/responsive/interfaces/responsivepluginconfig.md) - [Row_drag_handle_field](https://toolboxjs.com/grid/plugins/row-drag-drop/variables/row_drag_handle_field.md) - [Rowcommitdetail](https://toolboxjs.com/grid/plugins/editing/interfaces/rowcommitdetail.md) - [Rowcountpanel](https://toolboxjs.com/grid/plugins/pinned-rows/functions/rowcountpanel.md) - [Rowdragdropconfig](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowdragdropconfig.md) - [Rowdragdropplugin](https://toolboxjs.com/grid/plugins/row-drag-drop/classes/rowdragdropplugin.md) - [Rowdragenddetail](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowdragenddetail.md) - [Rowdragpayload](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowdragpayload.md) - [Rowdragstartdetail](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowdragstartdetail.md) - [Rowdropdetail](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowdropdetail.md) - [Rowmovedetail](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowmovedetail.md) - [Rowtransferdetail](https://toolboxjs.com/grid/plugins/row-drag-drop/interfaces/rowtransferdetail.md) - [Selectablecallback](https://toolboxjs.com/grid/plugins/selection/types/selectablecallback.md) - [Selectedcountpanel](https://toolboxjs.com/grid/plugins/pinned-rows/functions/selectedcountpanel.md) - [Selecteditorparams](https://toolboxjs.com/grid/plugins/editing/interfaces/selecteditorparams.md) - [Selectionaxis](https://toolboxjs.com/grid/plugins/selection/types/selectionaxis.md) - [Selectionchangedetail](https://toolboxjs.com/grid/plugins/selection/interfaces/selectionchangedetail.md) - [Selectionconfig](https://toolboxjs.com/grid/plugins/selection/interfaces/selectionconfig.md) - [Selectionmode](https://toolboxjs.com/grid/plugins/selection/types/selectionmode.md) - [Selectionplugin](https://toolboxjs.com/grid/plugins/selection/classes/selectionplugin.md) - [Selectionresult](https://toolboxjs.com/grid/plugins/selection/interfaces/selectionresult.md) - [Selectiontrigger](https://toolboxjs.com/grid/plugins/selection/types/selectiontrigger.md) - [Serversideconfig](https://toolboxjs.com/grid/plugins/server-side/interfaces/serversideconfig.md) - [Serversidedatasource](https://toolboxjs.com/grid/plugins/server-side/interfaces/serversidedatasource.md) - [Serversideplugin](https://toolboxjs.com/grid/plugins/server-side/classes/serversideplugin.md) - [Shellconfig](https://toolboxjs.com/grid/plugins/shell/interfaces/shellconfig.md) - [Shellheaderconfig](https://toolboxjs.com/grid/plugins/shell/interfaces/shellheaderconfig.md) - [Shellplugin](https://toolboxjs.com/grid/plugins/shell/classes/shellplugin.md) - [Sortmodel](https://toolboxjs.com/grid/plugins/multi-sort/interfaces/sortmodel.md) - [Stickypredicate](https://toolboxjs.com/grid/plugins/sticky-rows/types/stickypredicate.md) - [Stickyrowsconfig](https://toolboxjs.com/grid/plugins/sticky-rows/interfaces/stickyrowsconfig.md) - [Stickyrowsmode](https://toolboxjs.com/grid/plugins/sticky-rows/types/stickyrowsmode.md) - [Stickyrowsplugin](https://toolboxjs.com/grid/plugins/sticky-rows/classes/stickyrowsplugin.md) - [Subscribable](https://toolboxjs.com/grid/plugins/server-side/interfaces/subscribable.md) - [Texteditorparams](https://toolboxjs.com/grid/plugins/editing/interfaces/texteditorparams.md) - [Toolbarcontentdefinition](https://toolboxjs.com/grid/plugins/shell/interfaces/toolbarcontentdefinition.md) - [Toolpanelconfig](https://toolboxjs.com/grid/plugins/shell/interfaces/toolpanelconfig.md) - [Toolpaneldefinition](https://toolboxjs.com/grid/plugins/shell/interfaces/toolpaneldefinition.md) - [Tooltipconfig](https://toolboxjs.com/grid/plugins/tooltip/interfaces/tooltipconfig.md) - [Tooltipplugin](https://toolboxjs.com/grid/plugins/tooltip/classes/tooltipplugin.md) - [Touchselectionmode](https://toolboxjs.com/grid/plugins/selection/types/touchselectionmode.md) - [Treeconfig](https://toolboxjs.com/grid/plugins/tree/interfaces/treeconfig.md) - [Treeexpanddetail](https://toolboxjs.com/grid/plugins/tree/interfaces/treeexpanddetail.md) - [Treeloadchildrenparams](https://toolboxjs.com/grid/plugins/tree/interfaces/treeloadchildrenparams.md) - [Treeloadenddetail](https://toolboxjs.com/grid/plugins/tree/interfaces/treeloadenddetail.md) - [Treeloaderrordetail](https://toolboxjs.com/grid/plugins/tree/interfaces/treeloaderrordetail.md) - [Treeloadstartdetail](https://toolboxjs.com/grid/plugins/tree/interfaces/treeloadstartdetail.md) - [Treeplugin](https://toolboxjs.com/grid/plugins/tree/classes/treeplugin.md) - [Treerow](https://toolboxjs.com/grid/plugins/tree/types/treerow.md) - [Undoredoaction](https://toolboxjs.com/grid/plugins/undo-redo/types/undoredoaction.md) - [Undoredoconfig](https://toolboxjs.com/grid/plugins/undo-redo/interfaces/undoredoconfig.md) - [Undoredodetail](https://toolboxjs.com/grid/plugins/undo-redo/interfaces/undoredodetail.md) - [Undoredoplugin](https://toolboxjs.com/grid/plugins/undo-redo/classes/undoredoplugin.md) - [Viewportmappingquery](https://toolboxjs.com/grid/plugins/server-side/interfaces/viewportmappingquery.md) - [Viewportmappingresponse](https://toolboxjs.com/grid/plugins/server-side/interfaces/viewportmappingresponse.md) - [Visibilityconfig](https://toolboxjs.com/grid/plugins/visibility/interfaces/visibilityconfig.md) - [Visibilityplugin](https://toolboxjs.com/grid/plugins/visibility/classes/visibilityplugin.md) - [Zonedpanelrender](https://toolboxjs.com/grid/plugins/pinned-rows/interfaces/zonedpanelrender.md) ### Plugin Development - [Aftercellrendercontext](https://toolboxjs.com/grid/api/plugin-development/interfaces/aftercellrendercontext.md) - [Afterrowrendercontext](https://toolboxjs.com/grid/api/plugin-development/interfaces/afterrowrendercontext.md) - [Basegridplugin](https://toolboxjs.com/grid/api/plugin-development/classes/basegridplugin.md) - [Cellcontext](https://toolboxjs.com/grid/api/plugin-development/interfaces/cellcontext.md) - [Cellmouseevent](https://toolboxjs.com/grid/api/plugin-development/interfaces/cellmouseevent.md) - [Collectheaderrowscontext](https://toolboxjs.com/grid/api/plugin-development/interfaces/collectheaderrowscontext.md) - [Computescrollmapping](https://toolboxjs.com/grid/api/plugin-development/functions/computescrollmapping.md) - [Datagridelement Pluginapi](https://toolboxjs.com/grid/api/plugin-development/classes/datagridelement-pluginapi.md) - [Editorexeccontext](https://toolboxjs.com/grid/api/plugin-development/interfaces/editorexeccontext.md) - [Evalcontext](https://toolboxjs.com/grid/api/plugin-development/interfaces/evalcontext.md) - [Eventdefinition](https://toolboxjs.com/grid/api/plugin-development/interfaces/eventdefinition.md) - [Fromvirtualscrolltop](https://toolboxjs.com/grid/api/plugin-development/functions/fromvirtualscrolltop.md) - [Gridclassname](https://toolboxjs.com/grid/api/plugin-development/types/gridclassname.md) - [Gridcssvar](https://toolboxjs.com/grid/api/plugin-development/types/gridcssvar.md) - [Griddataattr](https://toolboxjs.com/grid/api/plugin-development/types/griddataattr.md) - [Gridplugin](https://toolboxjs.com/grid/api/plugin-development/interfaces/gridplugin.md) - [Gridselectors](https://toolboxjs.com/grid/api/plugin-development/variables/gridselectors.md) - [Headerrowcell](https://toolboxjs.com/grid/api/plugin-development/interfaces/headerrowcell.md) - [Headerrowcontribution](https://toolboxjs.com/grid/api/plugin-development/interfaces/headerrowcontribution.md) - [Hookname](https://toolboxjs.com/grid/api/plugin-development/types/hookname.md) - [Internalgrid](https://toolboxjs.com/grid/api/plugin-development/interfaces/internalgrid.md) - [Max_element_height_px](https://toolboxjs.com/grid/api/plugin-development/variables/max_element_height_px.md) - [Plugin Development](https://toolboxjs.com/grid/api/plugin-development.md) - [Plugindependency](https://toolboxjs.com/grid/api/plugin-development/interfaces/plugindependency.md) - [Pluginmanifest](https://toolboxjs.com/grid/api/plugin-development/interfaces/pluginmanifest.md) - [Pluginquery](https://toolboxjs.com/grid/api/plugin-development/interfaces/pluginquery.md) - [Querydefinition](https://toolboxjs.com/grid/api/plugin-development/interfaces/querydefinition.md) - [Renderphase](https://toolboxjs.com/grid/api/plugin-development/types/renderphase.md) - [Resizecontroller](https://toolboxjs.com/grid/api/plugin-development/interfaces/resizecontroller.md) - [Scrollmapping](https://toolboxjs.com/grid/api/plugin-development/interfaces/scrollmapping.md) - [Tovirtualscrolltop](https://toolboxjs.com/grid/api/plugin-development/functions/tovirtualscrolltop.md) - [Virtualstate](https://toolboxjs.com/grid/api/plugin-development/interfaces/virtualstate.md) ### Framework Adapters - [Datagridelement Adapters](https://toolboxjs.com/grid/api/framework-adapters/classes/datagridelement-adapters.md) - [Externalmounteditordetail](https://toolboxjs.com/grid/api/framework-adapters/interfaces/externalmounteditordetail.md) - [Externalmountviewdetail](https://toolboxjs.com/grid/api/framework-adapters/interfaces/externalmountviewdetail.md) - [Framework Adapters](https://toolboxjs.com/grid/api/framework-adapters.md) - [Frameworkadapter](https://toolboxjs.com/grid/api/framework-adapters/interfaces/frameworkadapter.md) ### Angular Adapter - [Applycolumndefaults](https://toolboxjs.com/grid/angular/api/utilities/applycolumndefaults.md) - [Basefilterpanel](https://toolboxjs.com/grid/angular/api/utilities/basefilterpanel.md) - [Basegrideditor](https://toolboxjs.com/grid/angular/api/utilities/basegrideditor.md) - [Basegrideditorcva](https://toolboxjs.com/grid/angular/api/utilities/basegrideditorcva.md) - [Baseoverlayeditor](https://toolboxjs.com/grid/angular/api/utilities/baseoverlayeditor.md) - [Cellcommitevent](https://toolboxjs.com/grid/angular/api/types/cellcommitevent.md) - [Celleditor](https://toolboxjs.com/grid/angular/api/types/celleditor.md) - [Cellrenderer](https://toolboxjs.com/grid/angular/api/types/cellrenderer.md) - [Columnconfig](https://toolboxjs.com/grid/angular/api/types/columnconfig.md) - [Columnfieldkey](https://toolboxjs.com/grid/angular/api/types/columnfieldkey.md) - [Columngroupdefinition](https://toolboxjs.com/grid/angular/api/types/columngroupdefinition.md) - [Columnshorthand](https://toolboxjs.com/grid/angular/api/types/columnshorthand.md) - [Createpluginfromfeature](https://toolboxjs.com/grid/angular/api/utilities/createpluginfromfeature.md) - [Editormounthook](https://toolboxjs.com/grid/angular/api/types/editormounthook.md) - [Exportmethods](https://toolboxjs.com/grid/angular/api/features/exportmethods.md) - [Featurename](https://toolboxjs.com/grid/angular/api/types/featurename.md) - [Filterconfig](https://toolboxjs.com/grid/angular/api/types/filterconfig.md) - [Filteringmethods](https://toolboxjs.com/grid/angular/api/features/filteringmethods.md) - [Filterpanel](https://toolboxjs.com/grid/angular/api/types/filterpanel.md) - [Formarraycontext](https://toolboxjs.com/grid/angular/api/types/formarraycontext.md) - [Getdetailtemplate](https://toolboxjs.com/grid/angular/api/utilities/getdetailtemplate.md) - [Geteditortemplate](https://toolboxjs.com/grid/angular/api/utilities/geteditortemplate.md) - [Getfeaturefactory](https://toolboxjs.com/grid/angular/api/utilities/getfeaturefactory.md) - [Getregisteredfeatures](https://toolboxjs.com/grid/angular/api/utilities/getregisteredfeatures.md) - [Getresponsivecardtemplate](https://toolboxjs.com/grid/angular/api/utilities/getresponsivecardtemplate.md) - [Getstructuraleditortemplate](https://toolboxjs.com/grid/angular/api/utilities/getstructuraleditortemplate.md) - [Gettoolpanelelements](https://toolboxjs.com/grid/angular/api/utilities/gettoolpanelelements.md) - [Gettoolpaneltemplate](https://toolboxjs.com/grid/angular/api/utilities/gettoolpaneltemplate.md) - [Grid](https://toolboxjs.com/grid/angular/api/directives/grid.md) - [Grid_icons](https://toolboxjs.com/grid/angular/api/utilities/grid_icons.md) - [Grid_type_defaults](https://toolboxjs.com/grid/angular/api/utilities/grid_type_defaults.md) - [Gridadapter](https://toolboxjs.com/grid/angular/api/adapters/gridadapter.md) - [Gridcellcontext](https://toolboxjs.com/grid/angular/api/types/gridcellcontext.md) - [Gridclipboarddirective](https://toolboxjs.com/grid/angular/api/directives/gridclipboarddirective.md) - [Gridcolumneditor](https://toolboxjs.com/grid/angular/api/directives/gridcolumneditor.md) - [Gridcolumnview](https://toolboxjs.com/grid/angular/api/directives/gridcolumnview.md) - [Gridcolumnvirtualizationdirective](https://toolboxjs.com/grid/angular/api/directives/gridcolumnvirtualizationdirective.md) - [Gridconfig](https://toolboxjs.com/grid/angular/api/types/gridconfig.md) - [Gridcontextmenudirective](https://toolboxjs.com/grid/angular/api/directives/gridcontextmenudirective.md) - [Griddetailcontext](https://toolboxjs.com/grid/angular/api/types/griddetailcontext.md) - [Griddetailview](https://toolboxjs.com/grid/angular/api/directives/griddetailview.md) - [Grideditingdirective](https://toolboxjs.com/grid/angular/api/directives/grideditingdirective.md) - [Grideditorcontext](https://toolboxjs.com/grid/angular/api/types/grideditorcontext.md) - [Gridexportdirective](https://toolboxjs.com/grid/angular/api/directives/gridexportdirective.md) - [Gridfilteringdirective](https://toolboxjs.com/grid/angular/api/directives/gridfilteringdirective.md) - [Gridformarray](https://toolboxjs.com/grid/angular/api/directives/gridformarray.md) - [Gridgroupingcolumnsdirective](https://toolboxjs.com/grid/angular/api/directives/gridgroupingcolumnsdirective.md) - [Gridgroupingrowsdirective](https://toolboxjs.com/grid/angular/api/directives/gridgroupingrowsdirective.md) - [Gridheadercontent](https://toolboxjs.com/grid/angular/api/directives/gridheadercontent.md) - [Gridheadercontentcontext](https://toolboxjs.com/grid/angular/api/types/gridheadercontentcontext.md) - [Gridheadercontext](https://toolboxjs.com/grid/angular/api/types/gridheadercontext.md) - [Gridheaderlabelcontext](https://toolboxjs.com/grid/angular/api/types/gridheaderlabelcontext.md) - [Gridiconregistry](https://toolboxjs.com/grid/angular/api/utilities/gridiconregistry.md) - [Gridlazyform](https://toolboxjs.com/grid/angular/api/directives/gridlazyform.md) - [Gridmasterdetaildirective](https://toolboxjs.com/grid/angular/api/directives/gridmasterdetaildirective.md) - [Gridmultisortdirective](https://toolboxjs.com/grid/angular/api/directives/gridmultisortdirective.md) - [Gridpinnedcolumnsdirective](https://toolboxjs.com/grid/angular/api/directives/gridpinnedcolumnsdirective.md) - [Gridpinnedrowsdirective](https://toolboxjs.com/grid/angular/api/directives/gridpinnedrowsdirective.md) - [Gridpivotdirective](https://toolboxjs.com/grid/angular/api/directives/gridpivotdirective.md) - [Gridprintdirective](https://toolboxjs.com/grid/angular/api/directives/gridprintdirective.md) - [Gridreordercolumnsdirective](https://toolboxjs.com/grid/angular/api/directives/gridreordercolumnsdirective.md) - [Gridresponsivecard](https://toolboxjs.com/grid/angular/api/directives/gridresponsivecard.md) - [Gridresponsivecardcontext](https://toolboxjs.com/grid/angular/api/types/gridresponsivecardcontext.md) - [Gridresponsivedirective](https://toolboxjs.com/grid/angular/api/directives/gridresponsivedirective.md) - [Gridrowdragdropdirective](https://toolboxjs.com/grid/angular/api/directives/gridrowdragdropdirective.md) - [Gridselectiondirective](https://toolboxjs.com/grid/angular/api/directives/gridselectiondirective.md) - [Gridserversidedirective](https://toolboxjs.com/grid/angular/api/directives/gridserversidedirective.md) - [Gridstickyrowsdirective](https://toolboxjs.com/grid/angular/api/directives/gridstickyrowsdirective.md) - [Gridtoolbarcontent](https://toolboxjs.com/grid/angular/api/directives/gridtoolbarcontent.md) - [Gridtoolbarcontentcontext](https://toolboxjs.com/grid/angular/api/types/gridtoolbarcontentcontext.md) - [Gridtoolpanel](https://toolboxjs.com/grid/angular/api/directives/gridtoolpanel.md) - [Gridtoolpanelcontext](https://toolboxjs.com/grid/angular/api/types/gridtoolpanelcontext.md) - [Gridtooltipdirective](https://toolboxjs.com/grid/angular/api/directives/gridtooltipdirective.md) - [Gridtreedirective](https://toolboxjs.com/grid/angular/api/directives/gridtreedirective.md) - [Gridtyperegistry](https://toolboxjs.com/grid/angular/api/utilities/gridtyperegistry.md) - [Gridundoredodirective](https://toolboxjs.com/grid/angular/api/directives/gridundoredodirective.md) - [Gridvisibilitydirective](https://toolboxjs.com/grid/angular/api/directives/gridvisibilitydirective.md) - [Groupingcolumnsconfig](https://toolboxjs.com/grid/angular/api/types/groupingcolumnsconfig.md) - [Groupingrowsconfig](https://toolboxjs.com/grid/angular/api/types/groupingrowsconfig.md) - [Hascolumnshorthands](https://toolboxjs.com/grid/angular/api/utilities/hascolumnshorthands.md) - [Injectgrid](https://toolboxjs.com/grid/angular/api/utilities/injectgrid.md) - [Injectgridexport](https://toolboxjs.com/grid/angular/api/features/injectgridexport.md) - [Injectgridfiltering](https://toolboxjs.com/grid/angular/api/features/injectgridfiltering.md) - [Injectgridprint](https://toolboxjs.com/grid/angular/api/features/injectgridprint.md) - [Injectgridreturn](https://toolboxjs.com/grid/angular/api/types/injectgridreturn.md) - [Injectgridselection](https://toolboxjs.com/grid/angular/api/features/injectgridselection.md) - [Injectgridundoredo](https://toolboxjs.com/grid/angular/api/features/injectgridundoredo.md) - [Iscomponentclass](https://toolboxjs.com/grid/angular/api/utilities/iscomponentclass.md) - [Isfeatureregistered](https://toolboxjs.com/grid/angular/api/utilities/isfeatureregistered.md) - [Lazyformfactory](https://toolboxjs.com/grid/angular/api/types/lazyformfactory.md) - [Masterdetailconfig](https://toolboxjs.com/grid/angular/api/types/masterdetailconfig.md) - [Nestedpaths](https://toolboxjs.com/grid/angular/api/types/nestedpaths.md) - [Normalizecolumns](https://toolboxjs.com/grid/angular/api/utilities/normalizecolumns.md) - [Overlayposition](https://toolboxjs.com/grid/angular/api/types/overlayposition.md) - [Panelrender](https://toolboxjs.com/grid/angular/api/types/panelrender.md) - [Panelslot](https://toolboxjs.com/grid/angular/api/types/panelslot.md) - [Parsecolumnshorthand](https://toolboxjs.com/grid/angular/api/utilities/parsecolumnshorthand.md) - [Pinnedrowsconfig](https://toolboxjs.com/grid/angular/api/types/pinnedrowsconfig.md) - [Pinnedrowslot](https://toolboxjs.com/grid/angular/api/types/pinnedrowslot.md) - [Pluginfactory](https://toolboxjs.com/grid/angular/api/types/pluginfactory.md) - [Printmethods](https://toolboxjs.com/grid/angular/api/features/printmethods.md) - [Providegrid](https://toolboxjs.com/grid/angular/api/utilities/providegrid.md) - [Providegridicons](https://toolboxjs.com/grid/angular/api/utilities/providegridicons.md) - [Providegridoptions](https://toolboxjs.com/grid/angular/api/types/providegridoptions.md) - [Providegridtypedefaults](https://toolboxjs.com/grid/angular/api/utilities/providegridtypedefaults.md) - [Registerfeature](https://toolboxjs.com/grid/angular/api/utilities/registerfeature.md) - [Responsivepluginconfig](https://toolboxjs.com/grid/angular/api/types/responsivepluginconfig.md) - [Rowcommitevent](https://toolboxjs.com/grid/angular/api/types/rowcommitevent.md) - [Rowformchangeevent](https://toolboxjs.com/grid/angular/api/types/rowformchangeevent.md) - [Selectionmethods](https://toolboxjs.com/grid/angular/api/features/selectionmethods.md) - [Tbweditor](https://toolboxjs.com/grid/angular/api/directives/tbweditor.md) - [Tbwgridcolumn](https://toolboxjs.com/grid/angular/api/directives/tbwgridcolumn.md) - [Tbwgridheader](https://toolboxjs.com/grid/angular/api/directives/tbwgridheader.md) - [Tbwgridtoolbuttons](https://toolboxjs.com/grid/angular/api/directives/tbwgridtoolbuttons.md) - [Tbwgridtype](https://toolboxjs.com/grid/angular/api/directives/tbwgridtype.md) - [Tbwheader](https://toolboxjs.com/grid/angular/api/directives/tbwheader.md) - [Tbwheaderlabel](https://toolboxjs.com/grid/angular/api/directives/tbwheaderlabel.md) - [Tbwrenderer](https://toolboxjs.com/grid/angular/api/directives/tbwrenderer.md) - [Typedefault](https://toolboxjs.com/grid/angular/api/types/typedefault.md) - [Typedefaultregistration](https://toolboxjs.com/grid/angular/api/types/typedefaultregistration.md) - [Undoredomethods](https://toolboxjs.com/grid/angular/api/features/undoredomethods.md) - [Zonedpanelrender](https://toolboxjs.com/grid/angular/api/types/zonedpanelrender.md) ### React Adapter - [Allfeatureprops](https://toolboxjs.com/grid/react/api/types/allfeatureprops.md) - [Applycolumndefaults](https://toolboxjs.com/grid/react/api/utilities/applycolumndefaults.md) - [Childfeaturedetector](https://toolboxjs.com/grid/react/api/types/childfeaturedetector.md) - [Clearfeatureregistry](https://toolboxjs.com/grid/react/api/utilities/clearfeatureregistry.md) - [Columnconfig](https://toolboxjs.com/grid/react/api/types/columnconfig.md) - [Columnfieldkey](https://toolboxjs.com/grid/react/api/types/columnfieldkey.md) - [Columngroupdefinition](https://toolboxjs.com/grid/react/api/types/columngroupdefinition.md) - [Columnoptions](https://toolboxjs.com/grid/react/api/types/columnoptions.md) - [Columnshorthand](https://toolboxjs.com/grid/react/api/types/columnshorthand.md) - [Createpluginfromfeature](https://toolboxjs.com/grid/react/api/utilities/createpluginfromfeature.md) - [Datagrid](https://toolboxjs.com/grid/react/api/components/datagrid.md) - [Datagridprops](https://toolboxjs.com/grid/react/api/types/datagridprops.md) - [Datagridref](https://toolboxjs.com/grid/react/api/types/datagridref.md) - [Detailpanelcontext](https://toolboxjs.com/grid/react/api/types/detailpanelcontext.md) - [Editormounthook](https://toolboxjs.com/grid/react/api/types/editormounthook.md) - [Eventhandler](https://toolboxjs.com/grid/react/api/types/eventhandler.md) - [Eventprops](https://toolboxjs.com/grid/react/api/types/eventprops.md) - [Exportmethods](https://toolboxjs.com/grid/react/api/features/exportmethods.md) - [Featurename](https://toolboxjs.com/grid/react/api/types/featurename.md) - [Featureprops](https://toolboxjs.com/grid/react/api/types/featureprops.md) - [Filterconfig](https://toolboxjs.com/grid/react/api/types/filterconfig.md) - [Filteringmethods](https://toolboxjs.com/grid/react/api/features/filteringmethods.md) - [Getfeaturefactory](https://toolboxjs.com/grid/react/api/utilities/getfeaturefactory.md) - [Getregisteredfeatures](https://toolboxjs.com/grid/react/api/utilities/getregisteredfeatures.md) - [Gridadapter](https://toolboxjs.com/grid/react/api/adapters/gridadapter.md) - [Gridcellcontext](https://toolboxjs.com/grid/react/api/types/gridcellcontext.md) - [Gridcolumn](https://toolboxjs.com/grid/react/api/components/gridcolumn.md) - [Gridcolumnprops](https://toolboxjs.com/grid/react/api/types/gridcolumnprops.md) - [Gridconfig](https://toolboxjs.com/grid/react/api/types/gridconfig.md) - [Griddetailcontext](https://toolboxjs.com/grid/react/api/types/griddetailcontext.md) - [Griddetailpanel](https://toolboxjs.com/grid/react/api/components/griddetailpanel.md) - [Griddetailpanelprops](https://toolboxjs.com/grid/react/api/types/griddetailpanelprops.md) - [Grideditorcontext](https://toolboxjs.com/grid/react/api/types/grideditorcontext.md) - [Gridheadercontent](https://toolboxjs.com/grid/react/api/components/gridheadercontent.md) - [Gridheadercontentprops](https://toolboxjs.com/grid/react/api/types/gridheadercontentprops.md) - [Gridiconprovider](https://toolboxjs.com/grid/react/api/utilities/gridiconprovider.md) - [Gridiconproviderprops](https://toolboxjs.com/grid/react/api/types/gridiconproviderprops.md) - [Gridprovider](https://toolboxjs.com/grid/react/api/utilities/gridprovider.md) - [Gridproviderprops](https://toolboxjs.com/grid/react/api/types/gridproviderprops.md) - [Gridresponsivecard](https://toolboxjs.com/grid/react/api/components/gridresponsivecard.md) - [Gridresponsivecardprops](https://toolboxjs.com/grid/react/api/types/gridresponsivecardprops.md) - [Gridtoolbarcontent](https://toolboxjs.com/grid/react/api/components/gridtoolbarcontent.md) - [Gridtoolbarcontentprops](https://toolboxjs.com/grid/react/api/types/gridtoolbarcontentprops.md) - [Gridtoolbuttons](https://toolboxjs.com/grid/react/api/components/gridtoolbuttons.md) - [Gridtoolbuttonsprops](https://toolboxjs.com/grid/react/api/types/gridtoolbuttonsprops.md) - [Gridtoolpanel](https://toolboxjs.com/grid/react/api/components/gridtoolpanel.md) - [Gridtoolpanelcontext](https://toolboxjs.com/grid/react/api/types/gridtoolpanelcontext.md) - [Gridtoolpanelprops](https://toolboxjs.com/grid/react/api/types/gridtoolpanelprops.md) - [Gridtype](https://toolboxjs.com/grid/react/api/utilities/gridtype.md) - [Gridtypeprops](https://toolboxjs.com/grid/react/api/types/gridtypeprops.md) - [Gridtypeprovider](https://toolboxjs.com/grid/react/api/utilities/gridtypeprovider.md) - [Gridtypeproviderprops](https://toolboxjs.com/grid/react/api/types/gridtypeproviderprops.md) - [Groupingcolumnsconfig](https://toolboxjs.com/grid/react/api/types/groupingcolumnsconfig.md) - [Groupingrowsconfig](https://toolboxjs.com/grid/react/api/types/groupingrowsconfig.md) - [Hascolumnshorthands](https://toolboxjs.com/grid/react/api/utilities/hascolumnshorthands.md) - [Isfeatureregistered](https://toolboxjs.com/grid/react/api/utilities/isfeatureregistered.md) - [Masterdetailconfig](https://toolboxjs.com/grid/react/api/types/masterdetailconfig.md) - [Nestedpaths](https://toolboxjs.com/grid/react/api/types/nestedpaths.md) - [Normalizecolumns](https://toolboxjs.com/grid/react/api/utilities/normalizecolumns.md) - [Panelrender](https://toolboxjs.com/grid/react/api/types/panelrender.md) - [Panelslot](https://toolboxjs.com/grid/react/api/types/panelslot.md) - [Parsecolumnshorthand](https://toolboxjs.com/grid/react/api/utilities/parsecolumnshorthand.md) - [Pinnedrowsconfig](https://toolboxjs.com/grid/react/api/types/pinnedrowsconfig.md) - [Pinnedrowslot](https://toolboxjs.com/grid/react/api/types/pinnedrowslot.md) - [Pluginfactory](https://toolboxjs.com/grid/react/api/types/pluginfactory.md) - [Postmountrefreshhook](https://toolboxjs.com/grid/react/api/types/postmountrefreshhook.md) - [Printmethods](https://toolboxjs.com/grid/react/api/features/printmethods.md) - [Registerchildfeaturedetector](https://toolboxjs.com/grid/react/api/utilities/registerchildfeaturedetector.md) - [Registereditormounthook](https://toolboxjs.com/grid/react/api/utilities/registereditormounthook.md) - [Registerfeature](https://toolboxjs.com/grid/react/api/utilities/registerfeature.md) - [Registerfeaturepropkey](https://toolboxjs.com/grid/react/api/utilities/registerfeaturepropkey.md) - [Registerpostmountrefresh](https://toolboxjs.com/grid/react/api/utilities/registerpostmountrefresh.md) - [Responsivecardcontext](https://toolboxjs.com/grid/react/api/types/responsivecardcontext.md) - [Responsivepluginconfig](https://toolboxjs.com/grid/react/api/types/responsivepluginconfig.md) - [Selectionmethods](https://toolboxjs.com/grid/react/api/features/selectionmethods.md) - [Toolpanelcontext](https://toolboxjs.com/grid/react/api/types/toolpanelcontext.md) - [Typedefault](https://toolboxjs.com/grid/react/api/types/typedefault.md) - [Typedefaultsmap](https://toolboxjs.com/grid/react/api/types/typedefaultsmap.md) - [Undoredomethods](https://toolboxjs.com/grid/react/api/features/undoredomethods.md) - [Usegrid](https://toolboxjs.com/grid/react/api/hooks/usegrid.md) - [Usegridexport](https://toolboxjs.com/grid/react/api/features/usegridexport.md) - [Usegridfiltering](https://toolboxjs.com/grid/react/api/features/usegridfiltering.md) - [Usegridicons](https://toolboxjs.com/grid/react/api/hooks/usegridicons.md) - [Usegridoverlay](https://toolboxjs.com/grid/react/api/hooks/usegridoverlay.md) - [Usegridoverlayoptions](https://toolboxjs.com/grid/react/api/types/usegridoverlayoptions.md) - [Usegridprint](https://toolboxjs.com/grid/react/api/features/usegridprint.md) - [Usegridreturn](https://toolboxjs.com/grid/react/api/types/usegridreturn.md) - [Usegridselection](https://toolboxjs.com/grid/react/api/features/usegridselection.md) - [Usegridtypedefaults](https://toolboxjs.com/grid/react/api/hooks/usegridtypedefaults.md) - [Usegridundoredo](https://toolboxjs.com/grid/react/api/features/usegridundoredo.md) - [Usetypedefault](https://toolboxjs.com/grid/react/api/hooks/usetypedefault.md) - [Zonedpanelrender](https://toolboxjs.com/grid/react/api/types/zonedpanelrender.md) ### Vue Adapter - [Allfeatureprops](https://toolboxjs.com/grid/vue/api/types/allfeatureprops.md) - [Applycolumndefaults](https://toolboxjs.com/grid/vue/api/utilities/applycolumndefaults.md) - [Celleditor](https://toolboxjs.com/grid/vue/api/types/celleditor.md) - [Cellrenderer](https://toolboxjs.com/grid/vue/api/types/cellrenderer.md) - [Cellslotprops](https://toolboxjs.com/grid/vue/api/types/cellslotprops.md) - [Clearfeatureregistry](https://toolboxjs.com/grid/vue/api/utilities/clearfeatureregistry.md) - [Columnconfig](https://toolboxjs.com/grid/vue/api/types/columnconfig.md) - [Columngroupdefinition](https://toolboxjs.com/grid/vue/api/types/columngroupdefinition.md) - [Columnshorthand](https://toolboxjs.com/grid/vue/api/types/columnshorthand.md) - [Createpluginfromfeature](https://toolboxjs.com/grid/vue/api/utilities/createpluginfromfeature.md) - [Detailpanelcontext](https://toolboxjs.com/grid/vue/api/types/detailpanelcontext.md) - [Editorslotprops](https://toolboxjs.com/grid/vue/api/types/editorslotprops.md) - [Exportmethods](https://toolboxjs.com/grid/vue/api/features/exportmethods.md) - [Featurename](https://toolboxjs.com/grid/vue/api/types/featurename.md) - [Featureprops](https://toolboxjs.com/grid/vue/api/types/featureprops.md) - [Filterconfig](https://toolboxjs.com/grid/vue/api/types/filterconfig.md) - [Filteringmethods](https://toolboxjs.com/grid/vue/api/features/filteringmethods.md) - [Getfeaturefactory](https://toolboxjs.com/grid/vue/api/utilities/getfeaturefactory.md) - [Getregisteredfeatures](https://toolboxjs.com/grid/vue/api/utilities/getregisteredfeatures.md) - [Grid_element_key](https://toolboxjs.com/grid/vue/api/utilities/grid_element_key.md) - [Grid_icons](https://toolboxjs.com/grid/vue/api/utilities/grid_icons.md) - [Grid_type_defaults](https://toolboxjs.com/grid/vue/api/utilities/grid_type_defaults.md) - [Gridadapter](https://toolboxjs.com/grid/vue/api/adapters/gridadapter.md) - [Gridconfig](https://toolboxjs.com/grid/vue/api/types/gridconfig.md) - [Gridiconprovider](https://toolboxjs.com/grid/vue/api/utilities/gridiconprovider.md) - [Gridiconproviderprops](https://toolboxjs.com/grid/vue/api/types/gridiconproviderprops.md) - [Gridprovider](https://toolboxjs.com/grid/vue/api/utilities/gridprovider.md) - [Gridproviderprops](https://toolboxjs.com/grid/vue/api/types/gridproviderprops.md) - [Gridtypeprovider](https://toolboxjs.com/grid/vue/api/utilities/gridtypeprovider.md) - [Gridtypeproviderprops](https://toolboxjs.com/grid/vue/api/types/gridtypeproviderprops.md) - [Groupingcolumnsconfig](https://toolboxjs.com/grid/vue/api/types/groupingcolumnsconfig.md) - [Groupingrowsconfig](https://toolboxjs.com/grid/vue/api/types/groupingrowsconfig.md) - [Hascolumnshorthands](https://toolboxjs.com/grid/vue/api/utilities/hascolumnshorthands.md) - [Headerlabelslotprops](https://toolboxjs.com/grid/vue/api/types/headerlabelslotprops.md) - [Headerslotprops](https://toolboxjs.com/grid/vue/api/types/headerslotprops.md) - [Isfeatureregistered](https://toolboxjs.com/grid/vue/api/utilities/isfeatureregistered.md) - [Isvuecomponent](https://toolboxjs.com/grid/vue/api/utilities/isvuecomponent.md) - [Masterdetailconfig](https://toolboxjs.com/grid/vue/api/types/masterdetailconfig.md) - [Normalizecolumns](https://toolboxjs.com/grid/vue/api/utilities/normalizecolumns.md) - [Panelrender](https://toolboxjs.com/grid/vue/api/types/panelrender.md) - [Panelslot](https://toolboxjs.com/grid/vue/api/types/panelslot.md) - [Parsecolumnshorthand](https://toolboxjs.com/grid/vue/api/utilities/parsecolumnshorthand.md) - [Pinnedrowsconfig](https://toolboxjs.com/grid/vue/api/types/pinnedrowsconfig.md) - [Pinnedrowslot](https://toolboxjs.com/grid/vue/api/types/pinnedrowslot.md) - [Pluginfactory](https://toolboxjs.com/grid/vue/api/types/pluginfactory.md) - [Printmethods](https://toolboxjs.com/grid/vue/api/features/printmethods.md) - [Registerfeature](https://toolboxjs.com/grid/vue/api/utilities/registerfeature.md) - [Responsivecardcontext](https://toolboxjs.com/grid/vue/api/types/responsivecardcontext.md) - [Responsivepluginconfig](https://toolboxjs.com/grid/vue/api/types/responsivepluginconfig.md) - [Selectionmethods](https://toolboxjs.com/grid/vue/api/features/selectionmethods.md) - [Toolpanelcontext](https://toolboxjs.com/grid/vue/api/types/toolpanelcontext.md) - [Typedefault](https://toolboxjs.com/grid/vue/api/types/typedefault.md) - [Typedefaultsmap](https://toolboxjs.com/grid/vue/api/types/typedefaultsmap.md) - [Undoredomethods](https://toolboxjs.com/grid/vue/api/features/undoredomethods.md) - [Usegrid](https://toolboxjs.com/grid/vue/api/composables/usegrid.md) - [Usegridexport](https://toolboxjs.com/grid/vue/api/features/usegridexport.md) - [Usegridfiltering](https://toolboxjs.com/grid/vue/api/features/usegridfiltering.md) - [Usegridicons](https://toolboxjs.com/grid/vue/api/composables/usegridicons.md) - [Usegridoverlay](https://toolboxjs.com/grid/vue/api/composables/usegridoverlay.md) - [Usegridoverlayoptions](https://toolboxjs.com/grid/vue/api/types/usegridoverlayoptions.md) - [Usegridprint](https://toolboxjs.com/grid/vue/api/features/usegridprint.md) - [Usegridreturn](https://toolboxjs.com/grid/vue/api/types/usegridreturn.md) - [Usegridselection](https://toolboxjs.com/grid/vue/api/features/usegridselection.md) - [Usegridtypedefaults](https://toolboxjs.com/grid/vue/api/composables/usegridtypedefaults.md) - [Usegridundoredo](https://toolboxjs.com/grid/vue/api/features/usegridundoredo.md) - [Usetypedefault](https://toolboxjs.com/grid/vue/api/composables/usetypedefault.md) - [Zonedpanelrender](https://toolboxjs.com/grid/vue/api/types/zonedpanelrender.md)