Skip to content

Angular Integration

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

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

Register the grid web component and enable the features you need:

// main.ts - Import grid and enable features
import '@toolbox-web/grid';
// Enable features via side-effect imports
// Only import the features you use - this keeps your bundle small!
import '@toolbox-web/grid-angular/features/selection';
import '@toolbox-web/grid-angular/features/editing';
import '@toolbox-web/grid-angular/features/multi-sort';
import '@toolbox-web/grid-angular/features/filtering';

The simplest way to use the grid is with the Grid directive and feature inputs:

// Enable features you use
import '@toolbox-web/grid-angular/features/selection';
import '@toolbox-web/grid-angular/features/editing';
import '@toolbox-web/grid-angular/features/multi-sort';
import '@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';
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
@Component({
selector: 'app-employee-grid',
imports: [Grid],
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 input enables a specific plugin with simplified configuration. See the Grid directive API for the full input/output reference.

Feature InputFeature ImportDescription
[selection]features/selectionCell, row, or range selection
[editing]features/editingInline cell editing
[multiSort]features/multi-sortMulti-column sorting
[filtering]features/filteringColumn filtering
[clipboard]features/clipboardCopy/paste support
[contextMenu]features/context-menuRight-click context menu
[reorderColumns]features/reorder-columnsColumn drag-to-reorder
[visibility]features/visibilityColumn visibility panel
[pinnedColumns]features/pinned-columnsSticky left/right columns
[pinnedRows]features/pinned-rowsSticky top/bottom rows
[groupingColumns]features/grouping-columnsMulti-level column headers
[groupingRows]features/grouping-rowsRow grouping
[columnVirtualization]features/column-virtualizationVirtualize columns for wide grids
[reorderRows]features/reorder-rowsRow drag-to-reorder
[tree]features/treeHierarchical tree view
[masterDetail]features/master-detailExpandable detail rows
[responsive]features/responsiveCard layout for narrow viewports
[undoRedo]features/undo-redoEdit undo/redo
[export]features/exportCSV/Excel export
[print]features/printPrint support
[pivot]features/pivotPivot table functionality
[serverSide]features/server-sideServer-side data loading

Core Config Inputs (no feature import needed):

InputTypeDescription
[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 the *tbwRenderer structural directive to customize how cell values are displayed. The directive context exposes $implicit (the cell value), row, and column.

import { Component, input } from '@angular/core';
import { Grid, TbwGridColumn, TbwRenderer } from '@toolbox-web/grid-angular';
// Standalone status badge component
@Component({
selector: 'app-status-badge',
standalone: true,
template: `
<span [class]="'badge badge--' + status()">
{{ status() }}
</span>
`,
styles: `
.badge { padding: 4px 8px; border-radius: 4px; }
.badge--active { background: #22c55e; color: white; }
.badge--inactive { background: #94a3b8; color: white; }
.badge--pending { background: #f59e0b; color: black; }
`
})
export class StatusBadgeComponent {
status = input.required<string>();
}
@Component({
imports: [Grid, TbwGridColumn, TbwRenderer, StatusBadgeComponent],
template: `
<tbw-grid [rows]="employees" [gridConfig]="config" style="height: 400px; display: block;">
<!-- Custom renderer for the status column -->
<tbw-grid-column field="status">
<app-status-badge *tbwRenderer="let value" [status]="value" />
</tbw-grid-column>
</tbw-grid>
`,
})
export class EmployeeGridComponent {
employees = [
{ id: 1, name: 'Alice', status: 'active' },
{ id: 2, name: 'Bob', status: 'inactive' },
{ id: 3, name: 'Charlie', status: 'pending' },
];
config = {
columns: [
{ field: 'id', header: 'ID', width: 70 },
{ field: 'name', header: 'Name' },
{ field: 'status', header: 'Status' },
],
};
}

Use the *tbwEditor structural directive for inline cell editing. Editor components emit commit and cancel events — the adapter handles wiring automatically.

import { Component, input, output } from '@angular/core';
import { Grid, TbwGridColumn, TbwRenderer, TbwEditor } from '@toolbox-web/grid-angular';
import '@toolbox-web/grid-angular/features/editing';
// Editor component - emits 'commit' with new value
@Component({
selector: 'app-status-editor',
standalone: true,
template: `
<select [value]="value()" (change)="commit.emit($any($event.target).value)">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
`,
})
export class StatusEditorComponent {
value = input<string>();
commit = output<string>(); // Auto-wired: emitting commits the edit
cancel = output<void>(); // Auto-wired: emitting cancels the edit
}
@Component({
imports: [Grid, TbwGridColumn, TbwRenderer, TbwEditor, StatusBadgeComponent, StatusEditorComponent],
template: `
<tbw-grid [rows]="employees" [gridConfig]="config" style="height: 400px; display: block;">
<tbw-grid-column field="status" editable>
<app-status-badge *tbwRenderer="let value" [status]="value" />
<app-status-editor *tbwEditor="let value" [value]="value" />
</tbw-grid-column>
</tbw-grid>
`,
})
export class EmployeeGridComponent {
employees = [/* ... */];
config = {
columns: [
{ field: 'id', header: 'ID', width: 70 },
{ field: 'name', header: 'Name', editable: true },
{ field: 'status', header: 'Status', editable: true },
],
features: { editing: true },
};
}

For complex editors (date pickers, overlays, form-integrated editors), see Base Classes.

Listen to grid events using standard Angular event binding:

@Component({
imports: [Grid],
template: `
<tbw-grid
[rows]="employees"
[gridConfig]="config"
(cell-click)="onCellClick($event)"
(row-click)="onRowClick($event)"
(cell-commit)="onCellCommit($event)"
(sort-change)="onSortChange($event)"
/>
`,
})
export class EmployeeGridComponent {
onCellClick(event: CustomEvent) {
const { row, column, value } = event.detail;
console.log('Cell clicked:', { row, column, value });
}
onRowClick(event: CustomEvent) {
const { row, rowIndex } = event.detail;
console.log('Row clicked:', row);
}
onCellCommit(event: CustomEvent) {
const { row, column, newValue, oldValue } = event.detail;
console.log('Cell edited:', { row, column, newValue, oldValue });
}
onSortChange(event: CustomEvent) {
const { sortState } = event.detail;
console.log('Sort changed:', sortState);
}
}

Access the underlying <tbw-grid> element via Angular’s viewChild:

import { Component, viewChild, ElementRef, effect } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { DataGridElement } from '@toolbox-web/grid';
@Component({
selector: 'app-employee-grid',
imports: [Grid],
template: `
<tbw-grid #grid [rows]="employees" [columns]="columns"></tbw-grid>
`,
})
export class EmployeeGridComponent {
private gridRef = viewChild<ElementRef<DataGridElement>>('grid');
constructor() {
effect(() => {
const grid = this.gridRef()?.nativeElement;
if (!grid) return;
grid.ready().then(() => {
console.log('Row count:', grid.sourceRows.length);
const selection = grid.getPluginByName('selection');
selection?.selectRow(0);
});
});
}
}

For a more Angular-idiomatic approach, use injectGrid() which provides a reactive API via signals:

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 '@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],
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');
}
}
FunctionImportKey Methods
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,
});
}
}

Override built-in icons globally using provideGridIcons():

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 GridDetailView for expandable detail panels with Angular templates:

import { Component } from '@angular/core';
import { Grid, GridDetailView } from '@toolbox-web/grid-angular';
import '@toolbox-web/grid-angular/features/master-detail';
import type { GridConfig } from '@toolbox-web/grid';
@Component({
imports: [Grid, GridDetailView],
template: `
<tbw-grid [rows]="employees" [gridConfig]="gridConfig">
<ng-template tbwDetailView let-row let-toggle="toggle">
<div class="detail-panel">
<h4>{{ row.name }}</h4>
<p>Email: {{ row.email }}</p>
<button (click)="toggle()">Close</button>
</div>
</ng-template>
</tbw-grid>
`,
})
export class EmployeeGridComponent {
employees = [/* ... */];
gridConfig: GridConfig = {
columns: [/* ... */],
features: { masterDetail: true },
};
}

Use GridResponsiveCard for mobile-friendly card layouts:

import { Component } from '@angular/core';
import { Grid, GridResponsiveCard } from '@toolbox-web/grid-angular';
import '@toolbox-web/grid-angular/features/responsive';
import type { GridConfig } from '@toolbox-web/grid';
@Component({
imports: [Grid, GridResponsiveCard],
template: `
<tbw-grid [rows]="employees" [gridConfig]="gridConfig">
<ng-template tbwResponsiveCard let-row let-columns="columns">
<div class="card">
<h3>{{ row.name }}</h3>
<p>{{ row.department }} · {{ row.email }}</p>
</div>
</ng-template>
</tbw-grid>
`,
})
export class EmployeeGridComponent {
employees = [/* ... */];
gridConfig: GridConfig = {
columns: [/* ... */],
features: { responsive: { breakpoint: 600, cardTitleField: 'name' } },
};
}

Use GridToolPanel for custom sidebar panels:

import { Component } from '@angular/core';
import { Grid, GridToolPanel } from '@toolbox-web/grid-angular';
import type { GridConfig } from '@toolbox-web/grid';
@Component({
imports: [Grid, GridToolPanel],
template: `
<tbw-grid [rows]="employees" [gridConfig]="gridConfig">
<ng-template tbwToolPanel="stats" let-rows>
<div class="stats-panel">
<h3>Statistics</h3>
<p>Total rows: {{ rows.length }}</p>
</div>
</ng-template>
</tbw-grid>
`,
})
export class EmployeeGridComponent {
employees = [/* ... */];
gridConfig: GridConfig = {
columns: [/* ... */],
shell: {
header: { title: 'Employees' },
toolPanels: [{ id: 'stats', label: 'Stats', icon: '📊' }],
},
};
}

Angular-specific pattern using inject(HttpClient) and signals:

import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Grid } from '@toolbox-web/grid-angular';
import '@toolbox-web/grid-angular/features/server-side';
import type { GridConfig, ServerDataSource } from '@toolbox-web/grid';
@Component({
imports: [Grid],
template: `<tbw-grid [rows]="employees()" [gridConfig]="gridConfig"></tbw-grid>`,
})
export class EmployeeGridComponent {
private http = inject(HttpClient);
employees = signal<Employee[]>([]);
private dataSource: ServerDataSource<Employee> = {
fetchRows: async (params) => {
const queryParams = new URLSearchParams();
if (params.sortModel?.length) {
queryParams.set('sort', params.sortModel[0].field);
queryParams.set('order', params.sortModel[0].direction);
}
if (params.filterModel) {
queryParams.set('filters', JSON.stringify(params.filterModel));
}
queryParams.set('offset', String(params.startRow));
queryParams.set('limit', String(params.endRow - params.startRow));
const response = await fetch(`/api/employees?${queryParams}`);
const data = await response.json();
return { rows: data.items, totalRows: data.total };
},
};
gridConfig: GridConfig<Employee> = {
columns: [
{ field: 'id', header: 'ID', sortable: true },
{ field: 'name', header: 'Name', sortable: true },
{ field: 'department', header: 'Department' },
],
features: { serverSide: { dataSource: this.dataSource } },
};
}

See the Server-Side plugin for the full ServerDataSource interface.

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