Angular Integration
The @toolbox-web/grid-angular package provides Angular integration for the <tbw-grid> data grid component.
Compatibility
Section titled “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
Section titled “Installation”npm install @toolbox-web/grid @toolbox-web/grid-angularyarn add @toolbox-web/grid @toolbox-web/grid-angularpnpm add @toolbox-web/grid @toolbox-web/grid-angularbun add @toolbox-web/grid @toolbox-web/grid-angularNo 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 below.
// 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
Section titled “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 |
Recommended: Per-Feature Directives
Section titled “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:
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
Section titled “Basic Usage”The simplest way to use the grid is with the Grid directive and per-feature directives:
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
Section titled “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.
| Per-Feature Directive (recommended) | Input | Feature Import | Description |
|---|---|---|---|
GridSelectionDirective | [selection] | features/selection | Cell, row, or range selection |
GridEditingDirective | [editing] | features/editing | Inline cell editing |
GridMultiSortDirective | [multiSort] | features/multi-sort | Multi-column sorting |
GridFilteringDirective | [filtering] | features/filtering | Column filtering |
GridClipboardDirective | [clipboard] | features/clipboard | Copy/paste support |
GridContextMenuDirective | [contextMenu] | features/context-menu | Right-click context menu |
GridReorderColumnsDirective | [reorderColumns] | features/reorder-columns | Column drag-to-reorder |
GridVisibilityDirective | [visibility] | features/visibility | Column visibility panel |
GridPinnedColumnsDirective | [pinnedColumns] | features/pinned-columns | Sticky left/right columns |
GridPinnedRowsDirective | [pinnedRows] | features/pinned-rows | Sticky top/bottom rows |
GridGroupingColumnsDirective | [groupingColumns] | features/grouping-columns | Multi-level column headers |
GridGroupingRowsDirective | [groupingRows] | features/grouping-rows | Row grouping |
GridColumnVirtualizationDirective | [columnVirtualization] | features/column-virtualization | Virtualize columns for wide grids |
GridRowDragDropDirective | [rowDragDrop] / [reorderRows] | features/row-drag-drop | Row drag-and-drop (within and across grids) |
GridTreeDirective | [tree] | features/tree | Hierarchical tree view |
GridMasterDetailDirective | [masterDetail] | features/master-detail | Expandable detail rows |
GridResponsiveDirective | [responsive] | features/responsive | Card layout for narrow viewports |
GridTooltipDirective | [tooltip] | features/tooltip | Cell / header tooltips |
GridUndoRedoDirective | [undoRedo] | features/undo-redo | Edit undo/redo |
GridExportDirective | [export] | features/export | CSV/Excel export |
GridPrintDirective | [print] | features/print | Print support |
GridPivotDirective | [pivot] | features/pivot | Pivot table functionality |
GridServerSideDirective | [serverSide] | features/server-side | Server-side data loading |
GridStickyRowsDirective | [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[] | Column definitions (alternative to gridConfig.columns) |
[gridConfig] | GridConfig | Full grid configuration object |
[loading] | boolean | Show the grid’s loading overlay |
[fitMode] | FitMode | 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
Section titled “Programmatic Grid Access”Use injectGrid() for programmatic access to the grid. It provides a reactive API via signals — see InjectGridReturn for the full return shape:
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
Section titled “Feature-Scoped Inject Functions”Feature imports export scoped inject functions for type-safe programmatic access to plugin functionality — no direct plugin references needed.
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
Section titled “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:
// Target a specific grid by CSS selectorgridSelection = injectGridSelection('tbw-grid.primary');gridExport = injectGridExport('#secondary-grid');| Function | Import | Key Methods |
|---|---|---|
injectGrid() | @toolbox-web/grid-angular | element, isReady, config, forceLayout, toggleGroup, registerStyles, visibleColumns |
injectGridSelection() | features/selection | selectAll, clearSelection, getSelection, selectedRowIndices (Signal) |
injectGridFiltering() | features/filtering | setFilter, clearAllFilters, getFilters, getFilteredRowCount |
injectGridExport() | features/export | exportToCsv, exportToExcel, exportToJson, isExporting |
injectGridPrint() | features/print | print, isPrinting |
injectGridUndoRedo() | features/undo-redo | undo, redo, canUndo (Signal), canRedo (Signal) |
Signal-Based Selection Example
Section titled “Signal-Based Selection Example”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
Section titled “Type-Level Defaults”Register application-wide renderers and editors for specific data types using provideGridTypeDefaults():
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:
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
Section titled “Custom Icons”The grid supports two complementary ways to customize icons — see the Theming Guide → 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, orHTMLElementinstances; takes precedence over CSS.
provideGridIcons() is the Angular DI wrapper for the JS path — it injects icons into gridConfig.icons for every grid in the application:
import { provideGridIcons } from '@toolbox-web/grid-angular';
export const appConfig = { providers: [ provideGridIcons({ sortAsc: '<svg>...</svg>', sortDesc: '<svg>...</svg>', filter: '<svg>...</svg>', }), ],};Manual Plugin Instantiation
Section titled “Manual Plugin Instantiation”While feature inputs are recommended, you can instantiate plugins manually for custom configurations or third-party plugins:
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
Section titled “Angular-Specific Directives”Custom Tool Panels
Section titled “Custom Tool Panels”Use the GridToolPanel 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):
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
Section titled “Header & Toolbar Content”Inject Angular components into the grid’s shell header (left zone) or toolbar (right zone) via the GridHeaderContent and GridToolbarContent directives. These wrap the imperative registerHeaderContent / registerToolbarContent APIs and mount an <ng-template> so projected components retain full DI, change detection, and signal reactivity.
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
Section titled “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 page (it has runnable demos and an Angular tab in every example). The two Angular-specific points worth calling out here:
- Use the per-feature directive (recommended). Add
GridServerSideDirectiveto your component’simportsif 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 onGriditself still works in v1.x but will be removed in v2.0. - Return the
HttpClientObservabledirectly fromgetRows. NofirstValueFrom, no manualtakeUntil. The grid subscribes once and callsunsubscribe()when the request is superseded (sort change, fast scrolling,refresh());HttpClientcancels the underlying XHR in response, which is the only correct way to avoid stale-buffer races.
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 theHttpClientcall in your own service (e.g. withshareReplay), or evaluate@tanstack/angular-query-experimental— note theexperimentaltag.
Dynamic Row Updates
Section titled “Dynamic Row Updates”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
Section titled “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
VIRTUALIZATIONphase runs instead of the fullROWSphase. - Selection preserved — the
SelectionPluginonly clears selection whensourceRowCountchanges, 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.
@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
Section titled “Performance Tips”OnPush Change Detection
Section titled “OnPush Change Detection”@Component({ changeDetection: ChangeDetectionStrategy.OnPush, // ...})export class EmployeeGridComponent { }Define Columns Once
Section titled “Define Columns Once”// ✅ Good — assigned oncereadonly columns: ColumnConfig[] = [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' },];
// ❌ Bad — getter creates new array every checkget columns() { return [{ field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }];}Troubleshooting
Section titled “Troubleshooting”Feature input not working
Section titled “Feature input not working”Ensure you imported the feature side-effect in your main.ts:
// main.ts — must be imported before bootstrapimport { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';“is not a known element” errors
Section titled ““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:
import { Grid, TbwGridColumn } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, TbwGridColumn], // Grid → <tbw-grid>, TbwGridColumn → <tbw-grid-column> // ...})Grid not rendering
Section titled “Grid not rendering”The grid needs a defined height for virtualization. See Troubleshooting → Height & Virtualization for solutions.
Row updates not reflected
Section titled “Row updates not reflected”The grid detects updates via reference equality. Always assign a new array:
// ✅ New reference — grid re-rendersthis.employees.update(rows => [...rows, newRow]);
// ❌ Same reference — grid won't detect changethis.employees().push(newRow);See Also
Section titled “See Also”- Base Classes —
BaseOverlayEditor,BaseGridEditorCVA,BaseFilterPanel - Reactive Forms —
FormArrayintegration withGridFormArrayandGridLazyForm - API Reference — Browse all directives, utilities, and types via the Angular API section in the sidebar
- Grid Plugins — All available plugins
- Core Features — Variable row heights, events, column config