Skip to content

Angular Integration

The @toolbox-web/grid-angular package provides Angular integration for the <tbw-grid> data grid component.

Angular versionSupport level
21Tested — used in demos and CI
20Tested
17 – 19Supported (minimum peer dependency)
< 17Not supported — adapter uses input() / output() signal APIs
Terminal window
npm install @toolbox-web/grid @toolbox-web/grid-angular
yarn add @toolbox-web/grid @toolbox-web/grid-angular
pnpm add @toolbox-web/grid @toolbox-web/grid-angular
bun add @toolbox-web/grid @toolbox-web/grid-angular

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 Three 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';

You can enable plugins on <tbw-grid> with one of three patterns. Pick whichever fits the component — they can be mixed.

PatternBest forv2 status
gridConfig.features (object literal)Configuration-driven apps; the entire grid setup lives in one object✅ Unchanged — fully supported
Per-feature directive (e.g. GridFilteringDirective)Template-driven apps that want signal inputs/outputs and the smallest bundleRecommended — the future-proof path
Inputs/outputs on Grid (e.g. [filtering], (filterChange))v1.x apps already on this style⚠️ Deprecated — bindings remain in v1.x for compatibility but will be removed in v2.0

gridConfig users are not affected by the v2 cleanup — only consumers binding feature inputs directly on <tbw-grid> need to switch to per-feature directives before v2.

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 existing attributes (tbw-grid[filtering], tbw-grid[filterChange]), so migrating from the deprecated style is a one-line imports addition per feature with zero template changes.

Legacy: Inputs/Outputs on Grid (deprecated)

Section titled “Legacy: Inputs/Outputs on Grid (deprecated)”

The v1.x style — declaring [filtering], (filterChange), etc. directly on the Grid directive without importing a per-feature directive — still works for the entire v1.x line but is @deprecated in JSDoc. v2.0 will remove these bindings from Grid, at which point a one-line directive import per feature is the migration.

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

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)InputFeature ImportDescription
GridSelectionDirective[selection]features/selectionCell, row, or range selection
GridEditingDirective[editing]features/editingInline cell editing
GridMultiSortDirective[multiSort]features/multi-sortMulti-column sorting
GridFilteringDirective[filtering]features/filteringColumn filtering
GridClipboardDirective[clipboard]features/clipboardCopy/paste support
GridContextMenuDirective[contextMenu]features/context-menuRight-click context menu
GridReorderColumnsDirective[reorderColumns]features/reorder-columnsColumn drag-to-reorder
GridVisibilityDirective[visibility]features/visibilityColumn visibility panel
GridPinnedColumnsDirective[pinnedColumns]features/pinned-columnsSticky left/right columns
GridPinnedRowsDirective[pinnedRows]features/pinned-rowsSticky top/bottom rows
GridGroupingColumnsDirective[groupingColumns]features/grouping-columnsMulti-level column headers
GridGroupingRowsDirective[groupingRows]features/grouping-rowsRow grouping
GridColumnVirtualizationDirective[columnVirtualization]features/column-virtualizationVirtualize columns for wide grids
GridRowDragDropDirective[rowDragDrop] / [reorderRows]features/row-drag-dropRow drag-and-drop (within and across grids)
GridTreeDirective[tree]features/treeHierarchical tree view
GridMasterDetailDirective[masterDetail]features/master-detailExpandable detail rows
GridResponsiveDirective[responsive]features/responsiveCard layout for narrow viewports
GridTooltipDirective[tooltip]features/tooltipCell / header tooltips
GridUndoRedoDirective[undoRedo]features/undo-redoEdit undo/redo
GridExportDirective[export]features/exportCSV/Excel export
GridPrintDirective[print]features/printPrint support
GridPivotDirective[pivot]features/pivotPivot table functionality
GridServerSideDirective[serverSide]features/server-sideServer-side data loading

Core Config Inputs (no feature import needed):

InputTypeDescription
[rows]T[]Row data — omit when using [serverSide]
[columns]ColumnConfig[]Column definitions (alternative to gridConfig.columns)
[gridConfig]GridConfigFull grid configuration object
[loading]booleanShow the grid’s loading overlay
[fitMode]FitModeColumn-fit strategy ('auto', 'fill', 'fill-rest', etc.)
[customStyles]stringCSS injected into the grid — useful for custom renderer/editor styling
[sortable]booleanGrid-wide sorting toggle (default: true)
[filterable]booleanGrid-wide filtering toggle (default: true). Requires FilteringPlugin.
[selectable]booleanGrid-wide selection toggle (default: true). Requires SelectionPlugin.

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

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 selector
gridSelection = injectGridSelection('tbw-grid.primary');
gridExport = injectGridExport('#secondary-grid');
FunctionImportKey Methods
injectGrid()@toolbox-web/grid-angularelement, isReady, config, forceLayout, toggleGroup, registerStyles, visibleColumns
injectGridSelection()features/selectionselectAll, clearSelection, getSelection, selectedRowIndices (Signal)
injectGridFiltering()features/filteringsetFilter, clearAllFilters, getFilters, getFilteredRowCount
injectGridExport()features/exportexportToCsv, exportToExcel, exportToJson, isExporting
injectGridPrint()features/printprint, isPrinting
injectGridUndoRedo()features/undo-redoundo, redo, canUndo (Signal), canRedo (Signal)
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 } },
};
}

Register application-wide renderers and editors for specific data types using provideGridTypeDefaults():

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:

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

The grid supports two complementary ways to customize icons — see the core Icon Customization reference and the Theming Guide for the full picture:

  • 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() is the Angular DI wrapper for the JS path — it injects icons into gridConfig.icons for every grid in the application:

app.config.ts
import { provideGridIcons } from '@toolbox-web/grid-angular';
export const appConfig = {
providers: [
provideGridIcons({
sortAsc: '<svg>...</svg>',
sortDesc: '<svg>...</svg>',
filter: '<svg>...</svg>',
}),
],
};

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';
@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.

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, GridToolPanel } from '@toolbox-web/grid-angular';
@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).

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:

  1. Use the per-feature directive (recommended). Add GridServerSideDirective 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.
import { inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { map } from 'rxjs/operators';
import type { GridConfig } from '@toolbox-web/grid';
import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side';
import { GridServerSideDirective } from '@toolbox-web/grid-angular/features/server-side';
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 — note the experimental tag.

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.

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
export class EmployeeGridComponent { }
// ✅ 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' }];
}

Ensure you imported the feature side-effect in your main.ts:

// main.ts — must be imported before bootstrap
import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';

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>
// ...
})

The grid needs a defined height for virtualization. See Troubleshooting → Height & Virtualization for solutions.

The grid detects updates via reference equality. Always assign a new array:

// ✅ New reference — grid re-renders
this.employees.update(rows => [...rows, newRow]);
// ❌ Same reference — grid won't detect change
this.employees().push(newRow);
  • Base ClassesBaseOverlayEditor, BaseGridEditorCVA, BaseFilterPanel
  • Reactive FormsFormArray integration with GridFormArray and GridLazyForm
  • API Reference — Complete API documentation for all directives and types
  • Grid Plugins — All available plugins
  • Core Features — Variable row heights, events, column config
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt