Angular data table guide
Use the official Angular wrapper in an existing Angular 17–22 application. This guide builds a typed, editable project table with signals, then shows how to replace local data with a cancellable server query.
Install in an Angular application
#Run this in an existing Angular application. The wrapper is standalone: no NgModule registration or global provider is required. This guide is checked with wts-data-table 1.1.1 and @wts-data-table/angular 1.0.0.
npm install wts-data-table@1.1.1 @wts-data-table/angular@1.0.0Load styles globally
#Add this once in src/styles.scss or src/styles.css. Do not put the library stylesheet inside an encapsulated component style. A parent component must import ProjectTableComponent and include <app-project-table /> in its template.
@import 'wts-data-table/styles.css';Define typed rows and editable columns
#Create table-data.ts beside your component. Stable project IDs preserve selection when rows change. Name and budget are editable; status supplies a select filter. The examples below use these three local rows and need no API key.
import type { DataTableOptions } from 'wts-data-table';
export interface Project {
id: string;
name: string;
status: string;
budget: number;
}
export const projects: Project[] = [
{ id: 'p1', name: 'Website redesign', status: 'In progress', budget: 24000 },
{ id: 'p2', name: 'Customer portal', status: 'Planning', budget: 18000 },
{ id: 'p3', name: 'Analytics dashboard', status: 'Complete', budget: 32000 }
];
export const columns: DataTableOptions<Project>['columns'] = [
{ accessor: 'name', header: 'Project', editable: true },
{ accessor: 'status', header: 'Status', filterVariant: 'select' },
{
accessor: 'budget', header: 'Budget', dataType: 'number',
editable: {
type: 'number', min: 0,
validate: value => Number.isFinite(Number(value)) && Number(value) >= 0
? undefined : 'Enter a non-negative budget.'
}
}
];Filter, select, and edit
#The complete component below enables global search, per-column filters, multiple row selection, and cell editing. Double-click a name or budget cell, or focus it and press Enter. Enter commits; Escape cancels. Negative budgets fail validation. These examples save edits in local application state only; a reload restores the sample data.
- showGlobalFilter and columnFilters control the search UI. Use controller.setGlobalFilter() or setColumnFilter() for your own controls.
- selectionMode enables checkboxes. onStateChange receives rowSelection IDs; the selected count here describes explicit selection in this local dataset, not an all-filtered server selection.
- For durable edits, await your authenticated API in onEditCommit before updating framework data. Reject or throw on failure so the table rolls back. Handle onEditError to display a useful message; validate and authorize again on the server.
Connect server-side data
#This is an optional integration recipe, not a hosted API. Create remote-data.ts and implement POST /api/projects/query on your backend. The request contains { state, filtering }; return { rows, rowCount }, where rowCount is the total after filtering, not the current page length. state.pagination.pageIndex is zero-based. The transport validates the response and forwards AbortSignal for cancellation.
- Process filtering, sorting, and pagination on the server when all three manual flags are true. The driver debounces queries, cancels superseded requests, caches results, and ignores stale responses.
- The following remote component is read-only. The data-source driver owns loaded rows; do not replace its stable empty input with the local sample array during rerenders.
- Require authentication and authorization, allowlist column IDs and operators, cap page size, and apply your application’s CSRF protection. Never interpolate untrusted field names into SQL.
- Loading and error phases appear in the status label. Refresh / retry forces a new request. Destroy the driver on unmount to abort requests and release subscriptions.
import type { DataTable } from 'wts-data-table';
import {
createDataTableDataSourceController,
type DataTableDataSourceResult,
type DataTableDataSourceStatus
} from 'wts-data-table/data-source';
import type { Project } from './table-data';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isProjectPage(value: unknown): value is DataTableDataSourceResult<Project> {
return isRecord(value) && Array.isArray(value.rows)
&& typeof value.rowCount === 'number' && Number.isSafeInteger(value.rowCount)
&& value.rowCount >= value.rows.length
&& value.rows.every(row => isRecord(row)
&& typeof row.id === 'string' && typeof row.name === 'string'
&& typeof row.status === 'string' && typeof row.budget === 'number'
&& Number.isFinite(row.budget));
}
export function connectProjects(
table: DataTable<Project>,
onStatusChange: (status: DataTableDataSourceStatus) => void
) {
return createDataTableDataSourceController<Project>({
table: table.core,
debounceMs: 200,
onStatusChange,
async dataSource({ state, filtering, signal }) {
const response = await fetch('/api/projects/query', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state, filtering }),
signal
});
if (!response.ok) throw new Error('Project request failed: ' + response.status);
const page: unknown = await response.json();
if (!isProjectPage(page)) throw new Error('Invalid project response');
return page;
}
});
}Mount the remote Angular table
#Use this instead of the local component when your backend is ready. The ready output supplies the controller. The driver is released both before reconnecting and when the owning component is destroyed. Import RemoteProjectTableComponent in a parent and render <app-remote-project-table />.
import { Component, signal, type OnDestroy } from '@angular/core';
import type { DataTable } from 'wts-data-table';
import {
WtsDataTableAngularComponent,
type WtsAngularDataTableOptions
} from '@wts-data-table/angular';
import { columns, type Project } from './table-data';
import { connectProjects } from './remote-data';
@Component({
selector: 'app-remote-project-table',
standalone: true,
imports: [WtsDataTableAngularComponent],
template: `
<p role="status">{{ status() }}</p>
<button type="button" (click)="refresh()">Refresh / retry</button>
<wts-data-table-angular [data]="initialRows" [options]="options"
(ready)="connect($event)" (destroyed)="disconnect()" />
`
})
export class RemoteProjectTableComponent implements OnDestroy {
readonly initialRows: readonly Project[] = [];
readonly status = signal('idle');
private remote?: ReturnType<typeof connectProjects>;
readonly options: WtsAngularDataTableOptions<Project> = {
columns, getRowId: row => row.id, ariaLabel: 'Remote projects',
showGlobalFilter: true, columnFilters: 'always', editing: false,
manualFiltering: true, manualSorting: true, manualPagination: true,
initialState: { pagination: { pageSize: 25 } }
};
connect(table: DataTable<Project>): void {
this.disconnect();
this.remote = connectProjects(table, ({ phase }) => this.status.set(phase));
}
refresh(): void { void this.remote?.refresh(true); }
disconnect(): void { this.remote?.destroy(); this.remote = undefined; }
ngOnDestroy(): void { this.disconnect(); }
}Angular lifecycle and controlled state
#A new data array updates the existing controller. A new options object remounts it, so keep options as a stable readonly field and use controller methods for interaction updates. The wrapper creates DOM only in the browser and owns table destruction.
- Use (ready) to access the typed controller, (stateChange) for state snapshots, and (stateChanged) when you need the reason and controller. [(state)] is supported for application-controlled state.
- Use signals for callback-driven application UI, including zoneless Angular. When supplying custom cell UI, import WtsDataTableCellTemplateDirective and use an ng-template wtsDataTableCell="columnId".
- Keep API subscriptions and extra data-source drivers under your component’s teardown; the wrapper cannot dispose application-owned services.
Troubleshoot Angular integration
#Start with the local component before adding your API.
- Unknown element: import WtsDataTableAngularComponent into the standalone component that uses it.
- Unstyled table: load the CSS globally, not only in component-scoped styles.
- Selection resets: check stable getRowId values and avoid constructing options in a template getter.
- No remote rows: inspect the HTTP status and response shape; supply the filtered total rowCount and ensure your server honors the manual query flags.
project-table.component.ts
#Complete local component. Add table-data.ts from the typed-rows section, import the global stylesheet, then import ProjectTableComponent in a parent and render <app-project-table />. No backend is needed for this example.
import { Component, signal } from '@angular/core';
import {
WtsDataTableAngularComponent,
type WtsAngularDataTableOptions
} from '@wts-data-table/angular';
import { columns, projects, type Project } from './table-data';
@Component({
selector: 'app-project-table',
standalone: true,
imports: [WtsDataTableAngularComponent],
template: `
<p aria-live="polite">{{ selected() }} selected</p>
<wts-data-table-angular [data]="rows()" [options]="options" />
`
})
export class ProjectTableComponent {
readonly rows = signal<readonly Project[]>(projects);
readonly selected = signal(0);
readonly options: WtsAngularDataTableOptions<Project> = {
columns,
getRowId: row => row.id,
ariaLabel: 'Project delivery',
showGlobalFilter: true,
columnFilters: 'always',
selectionMode: 'multiple',
editing: true,
initialState: { pagination: { pageSize: 10 } },
onStateChange: state => this.selected.set(state.rowSelection.length),
// Local-only edits: keep the Angular input in sync with the controller.
onEditCommit: ({ instance }) => this.rows.set([...instance.getData()])
};
}Explore search, selection, editing, and runtime options in the interactive example.
Open the configurable example →Ready to try it with real data?
Explore configurable examples ↗