agrid
June 25, 2026 · View on GitHub
agrid is an Angular data grid with spreadsheet-like editing, virtual scrolling, filtering, sorting, grouping, column state, pinned columns, selection, clipboard workflows, row operations, pagination, CSV/Excel export, charts/graphs, and custom cell renderers.

Live Demo
Quick Start
npm install @thkl/agrid @angular/cdk
import { Component } from '@angular/core';
import { AgridComponent, AgridControl, AgridDataSource, AgridProvider, ColDef, GridEditEvent } from '@thkl/agrid';
const columns: ColDef[] = [
{ field: 'id', header: 'ID', width: 70, editable: false, pinned: 'left' },
{ field: 'name', header: 'Name', width: 160, filterable: true },
{ field: 'hiredAt', header: 'Hire Date', width: 130 }, // auto-formatted as a date
{ field: 'departmentId', header: 'Department', width: 140, filterable: true, groupable: true,
values: [
{ value: 1, label: 'Engineering' },
{ value: 2, label: 'Sales' },
],
},
];
@Component({
selector: 'app-page',
imports: [AgridComponent],
template: `
<agrid [provider]="gridProvider" (cellEdit)="onCellEdit($event)" />
`,
})
export class PageComponent {
readonly columns = columns;
readonly ds = new AgridDataSource([
{ id: 1, name: 'Alice', hiredAt: '2021-03-15', departmentId: 1 },
{ id: 2, name: 'Bob', hiredAt: '2022-07-01', departmentId: 2 },
]);
readonly gridControl = new AgridControl({ allowRowReorder: true });
readonly gridProvider = new AgridProvider({
locale: 'en-US',
columns: this.columns,
datasource: this.ds,
control: this.gridControl,
showControlColumn: true,
showSidebar: true,
zebraStripes: true,
rowSelection: 'multi',
});
onCellEdit(event: GridEditEvent): void {
console.log(event);
}
}
Features
See ROADMAP.md for the AG Grid comparison checklist and open parity items.
- Angular 21 standalone component.
- CDK virtual scrolling for large row sets.
- Signal-based data source and control state.
- Editable text cells and select editors for fixed value columns.
- Custom component cell editors via
ColDef.cellEditor(star ratings, pickers, sliders) with zero extra dependencies. - Keyboard navigation with auto-scroll to the active cell.
- Type-to-edit, Enter/F2 edit, Tab/Enter commit, Escape cancel.
- Undo/redo for edits, paste, and fill operations.
- Cell range selection with Shift+arrow, Shift+click, or left-button drag with edge auto-scroll.
- Clipboard copy/paste using TSV/CSV-like plain text.
- Live selection status bar with count, sum, average, minimum, and maximum.
- Fill handle for repeating selected cell/range values down or right.
- Find panel with Ctrl/Cmd+F, full filtered-dataset matching, and next/previous navigation.
- Quick filter for searching across all visible columns.
- Text filters, string/number/date condition filters, value filters, and single-column sorting.
- Server-side filter/sort events for remote data workflows.
- Column menu with sort, clear sort, autosize, pin/unpin, hide, group, and clear filter actions.
- Column resizing by drag and autosize by double-click.
- Column reordering by header drag.
- Column virtualization for very wide grids.
- Conditional cell formatting with colors, typography, borders, and alignment.
- Row-aware horizontal cell spanning, clamped within each pinned or scrollable pane.
- Split-pane pinned columns on the left.
- Optional control column for row context actions, row numbers, row marking, and row reordering.
- Column marking for complete-column highlighting and clipboard workflows.
- Row selection: none, single, or multi.
- Grouping with expand/collapse and custom group actions.
- Sidebar column visibility picker.
- Add-row placeholder and automatic row insertion.
- CSV and zero-dependency Excel (
.xlsx) export of visible, filtered data rows. - Server-side row model with lazy block loading and virtual placeholders.
- Date auto-formatting — ISO strings and
Dateobjects are detected and displayed as locale-formatted dates automatically. - Zebra stripes — alternating row shading for easier reading.
- Readonly mode — disable all editing with a single input.
- Pagination — built-in page controls driven by
AgridControl. - Pivot tables — derive read-only cross-tab views from row, column, value, and aggregate fields.
- Tree node aggregates — roll up sum, average, count, or custom values into expandable tree nodes.
- Custom cell renderers — render any Angular component per column for rich cell content (the legacy HTML-string renderer is deprecated).
- Custom cell editors — render any Angular component while editing; the grid keeps validation, history, and the commit lifecycle.
- Charts / graphs — zero-dependency SVG column/bar/line/area/pie/donut diagrams via
<agrid-chart>, configured with anAgridChartProvider. Link to a grid'svisibleRowsto follow filters and sorting live. - Sparklines — inline SVG line and bar charts rendered per row from numeric series data.
- Column autosize all — fit every visible column to its content in one call.
- Master/detail rows — expand any row to reveal a custom HTML detail panel beneath it.
- Pinned rows — keep summary/total rows fixed at the top or bottom of the body.
- Persistable settings — save and restore serializable column, filter, sidebar, and pivot state.
- Row CSS classes — apply conditional classes to whole rows via
getRowClass.
Component API
<agrid [provider]="gridProvider" (cellEdit)="onEdit($event)" />
AgridComponent has a single input: provider. All grid options, data, and control state are supplied through AgridProvider. See AgridProvider Configuration for the full option list.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
provider | AgridProvider | New empty provider | Supplies column definitions, data source, control state, and all grid options. |
Localization
Set locale on AgridProvider to control built-in grid text and date formatting. Built-in text supports English (en-*) and German (de-*).
The default is 'auto', which reads navigator.language from the browser and falls back to 'en-US' if the browser language is not supported.
// Auto-detect browser language (default — no need to set locale explicitly)
readonly gridProvider = new AgridProvider({ ... });
// Pin to a specific locale
readonly gridProvider = new AgridProvider({ locale: 'de-DE', ... });
Adding custom locale text
Use addLocalization(locale, overrides) to register label overrides for one or more locales. When locale is 'auto', the grid matches the browser language against all registered locales — exact match first, then primary-language match (e.g. a registered 'fr' locale matches a browser locale of 'fr-FR' or 'fr-BE').
readonly gridProvider = new AgridProvider({ ... })
.addLocalization('fr-FR', {
addRow: 'Ajouter une ligne',
noRows: 'Aucune donnée',
rows: count => `${count} enregistrement${count === 1 ? '' : 's'}`,
groupBy: header => `Grouper par ${header}`,
})
.addLocalization('nl-NL', {
addRow: 'Rij toevoegen',
noRows: 'Geen rijen',
});
addLocalization returns the provider so calls can be chained. Partial overrides are merged on top of the built-in base bundle for that locale — you only need to supply the labels you want to change.
The AgridLocaleTextOverrides type covers all overridable labels.
Outputs
| Output | Type | Description |
|---|---|---|
cellEdit | GridEditEvent | Emitted after a committed cell edit, paste, fill, undo, or redo changes a cell. |
recordEdit | RecordEditEvent | Emitted on the next microtask after an edit updates a row. Includes the row index, current data, exact provider, and datasource. |
rowChanged | RowUpdateEvent | Emitted once with the latest row after inline editing leaves that row, or when the sidebar editor Save button is used. Use this for one API request after several field edits. |
rowRemoved | RecordEditEvent | Emitted after deleting a row. Includes its former index, captured data, exact provider, and datasource. |
prepareAddRecord | NewRecord | Emitted after the grid inserts a blank row. Patch event.datasource to target the correct grid when multiple providers are rendered. |
rowReorder | RowReorderEvent | Emitted after the user drops a reordered row. The host must call dataSource.moveRow(). |
rowSelect | RowSelectEvent | null | Emitted when row selection changes. null means selection was cleared. |
cellSelect | CellSelectEvent<T> | null | Emitted when cell selection changes. null means selection was cleared. |
rowMark | RowMarkEvent<T> | Emitted after the row-header surface or marker checkbox marks or unmarks a row. |
columnMark | ColumnMarkEvent<T> | Emitted after a header marks or unmarks a complete column. |
columnHeaderAction | ColumnHeaderActionEvent<T> | Emitted for a custom column-menu command with { column, key }. |
firstDataRendered | FirstDataRenderedEvent<T> | Emitted once after the first completed render containing datasource rows. |
menuBarAction | string | Emitted for every enabled menu-bar button or dropdown item with its configured id. |
treeNodeClick | TreeNodeClickEvent | Emitted when a generated path-tree branch node is clicked. |
treeNodeDoubleClicked | TreeNodeClickEvent | Emitted when a generated path-tree branch node is double-clicked. |
cellInfo | CellInfoEvent<T> | Emitted when a column's optional cell info icon is clicked. |
filterChange | FilterChangeEvent | Emitted for text filter changes when serverSideFiltering is enabled. |
sortChange | SortChangeEvent | Emitted for sort changes when serverSideFiltering is enabled. |
Use rowChanged instead of cellEdit when an API should receive the complete row only after the
user finishes editing it:
<agrid [provider]="provider" (rowChanged)="saveRow($event)" />
saveRow(event: RowUpdateEvent<PersonRow>): void {
this.http.patch(`/api/people/${event.row.id}`, event.row).subscribe(() => {
this.provider.control.indicate(event.originalIndex, '#2da44e', 1000);
this.provider.control.clearChangedCells(event.originalIndex);
});
}
During inline editing, moving between fields in the same row does not emit rowChanged. The event
fires when navigation enters another row, filter focus clears the active cell, or focus leaves the
grid. recordEdit and cellEdit continue to fire for each committed field mutation.
Enable changed-cell markers when the user should see which values are waiting to be persisted:
readonly provider = new AgridProvider<PersonRow>({
columns,
datasource,
showChangedCellIndicator: true,
});
After a successful API request, call control.clearChangedCells(index) for the complete row,
control.clearChangedCells(index, ['name', 'email']) for selected fields, or
control.clearChangedCells() for every marker. Call control.indicate(index, color, durationMs)
to flash a complete row for transient server-side feedback.
AgridProvider Configuration
All grid options are passed to AgridProvider at construction time:
readonly gridProvider = new AgridProvider({
columns: this.columns,
datasource: this.ds,
control: this.gridControl,
zebraStripes: true,
showSidebar: true,
showControlColumn: true,
rowSelection: 'multi',
allowAddRows: true,
enableRowMarking: true,
confirmRowDelete: true,
readonly: false,
});
| Option | Type | Default | Description |
|---|---|---|---|
columns | ColDef[] | [] | Column definitions. |
headerGroups | HeaderGroup[] | [] | Labels for optional grouped column headers. |
datasource | AgridDataSource | New empty datasource | Row data container. |
serverSideRowModel | AgridServerSideRowModel | undefined | Lazy block-based datasource with virtual placeholders, caching, and automatic filter/sort query forwarding. |
control | AgridControl | New default control | Manages filters, sort, grouping, pagination, and undo/redo. |
locale | string | 'auto' | BCP-47 locale tag for grid text and date formatting. 'auto' reads navigator.language and falls back to 'en-US'. |
localization | AgridLocaleTextOverrides | undefined | Overrides individual labels. See Localization. |
rowHeight | number | 32 | Fixed row height in pixels. Required by CDK virtual scroll. |
minHeight | string | undefined | CSS min-height for the virtual body. Example: '200px'. |
maxHeight | string | undefined | CSS max-height for the virtual body. Example: '500px'. |
allowAddRows | boolean | false | Shows a + Add row placeholder at the bottom when autoAddRows is false. |
autoAddRows | boolean | false | Automatically inserts a blank row when navigation moves past the last real row. |
showControlColumn | boolean | false | Shows a 24 px control column for row context actions and drag handles. |
showRowNumbers | boolean | false | Shows 1-based filtered/sorted row numbers in the control column, replacing the drag-handle glyph. |
enableRowMarking | boolean | false | Makes row headers clickable, shows checkboxes in a 48 px control column, and includes marked rows in every copy operation. |
enableColumnMarking | boolean | false | Makes column-header surfaces clickable and exposes marked fields through markedColumnFields. |
showSidebar | boolean | false | Shows a collapsible column visibility sidebar. Requires control. |
autoOpenDetail | boolean | false | Opens the detail row automatically when a row is selected. |
serverSideFiltering | boolean | false | Emits filter/sort events instead of applying them locally and hides the value checklist. |
filterDebounceMs | number | 300 | Debounce delay for server-side filterChange events. Set to 0 to disable. |
menuBarItems | AgridMenuBarItem<T>[] | [] | Optional buttons above the headers. Buttons may expose additional dropdown commands. |
sortOption | 'single' | 'multi' | 'none' | 'multi' | Allows one sort, multiple sorts, or disables sorting. |
rowSelection | 'single' | 'multi' | 'none' | 'none' | Row selection behavior. |
enterEditAction | 'nothing' | 'nextColumn' | 'nextRow' | 'nextRow' | Behavior after pressing Enter while editing a cell. |
groupDescription | ((label: string) => string) | null | null | Optional description text shown next to each group label. |
groupActions | GroupAction[] | [] | Actions shown in each group header menu. |
cellMenuItems | (CellContextMenuItem | null)[] | [] | Additional items in the cell right-click context menu. null inserts a divider. |
zebraStripes | boolean | false | Shades every other row. Override --agrid-color-bg-stripe to change the shade. |
columnVirtualizationThreshold | number | 30 | Renders only the scrollable columns near the horizontal viewport once the scrollable-column count exceeds this value. Lower it to virtualize sooner, or set Infinity to disable. Pinned columns always render. |
showChangedCellIndicator | boolean | false | Marks committed cell changes until clearChangedCells() is called. |
confirmRowDelete | boolean | false | Fades the target row and shows a localized in-row Yes/No confirmation. |
emptyText | string | undefined | Text shown when the grid has no rows. Falls back to the locale default. |
readonly | boolean | false | Initial value for the readonly signal. Makes all cells non-editable. |
loading | boolean | false | Initial value for control.loading. Shows a loading overlay over the grid body. |
getRowClass | (p: { row; index }) => string | undefined | Returns CSS class names applied to a whole data row. Complements ColDef.cellClass. |
pinRow | (row, index) => 'top' | 'bottom' | undefined | undefined | Pins matching rows to the top/bottom of the body (see Master/Detail and Pinned Rows). |
treeConfig | AgridTreeConfig<T> | null | null | Builds a tree from id/parent-id accessors or getPath segments. Supports descendant rollups through aggregateTreeNodes. Path labels and branch UUIDs can be customized with formatPathSegment and nodeUuid. |
pivotConfig | AgridPivotConfig<T> | null | null | Derives a read-only client-side pivot from one row field, one column field, and one aggregated value field. |
masterDetail | boolean | false | Enables expandable detail panels. In tree mode, only leaf rows can expand details. Not available while grouped. |
detailRenderer | (p: { row }) => string | undefined | Returns sanitized HTML for an expanded detail panel. |
detailColumnField | column field | undefined | Shows one linked column as a multiline editable field in the detail panel. |
detailActions | { id; label; text? }[] | [] | Adds template buttons above the linked detail textarea. text may be a string or (p: { row; rowIndex }) => string. |
detailRowHeight | number | 200 | Fixed height in pixels of an expanded detail panel. |
Tree grids and descendant rollups
Set aggregateTreeNodes to display aggregate-column values on every expandable tree node. The
grid uses each column's existing aggregate function, so footer and tree aggregation share the
same sum/average/minimum/maximum/count or custom-function configuration.
const columns: ColDef<OrgRow>[] = [
{ field: 'name', header: 'Name' },
{ field: 'amount', header: 'Amount', type: 'number', aggregate: 'sum' },
];
const treeConfig: AgridTreeConfig<OrgRow> = {
getId: row => row.id,
getParentId: row => row.parentId,
treeField: 'name',
aggregateTreeNodes: true,
};
Rollups use descendant leaves, not intermediate parent values, which avoids double-counting stored
subtotals in multi-level trees. Collapsed descendants continue to contribute. Active filters
recalculate rollups over the filtered tree, including ancestors retained by
keepAncestorsOnFilter. Parent aggregate cells are display-only and do not overwrite source data.
Generated getPath branches use all datasource leaves beneath the branch and show their rollups
inline.
Standalone tree
AgridTreeComponent and AgridTreeProvider provide the same hierarchy without grid columns.
The control accepts AgridTreeConfig<T>, supports keyboard navigation and selection, and emits
normalized row/path-branch events. Because the standalone tree has no columns, it ignores
aggregateTreeNodes.
readonly treeProvider = new AgridTreeProvider<Node>({
datasource: new AgridDataSource(nodes),
treeConfig: {
getId: node => node.id,
getParentId: node => node.parentId,
treeField: 'name',
defaultExpanded: true,
},
getDescription: node => node.type,
});
<agrid-tree [provider]="treeProvider" (nodeClick)="openNode($event)" />
Client-side pivot
The first pivot slice creates a read-only table from one row dimension, one column dimension, and one value field:
readonly provider = new AgridProvider<Sale>({
columns: [
{ field: 'region', header: 'Region' },
{ field: 'quarter', header: 'Quarter' },
{ field: 'revenue', header: 'Revenue', type: 'number' },
],
datasource: new AgridDataSource(sales),
pivotConfig: {
rowField: 'region',
columnField: 'quarter',
valueField: 'revenue',
aggregate: 'sum',
},
});
Supported aggregates are sum (default), avg, min, max, count, and custom functions.
Source updates regenerate the pivot. Filtering, sorting, selection, and pagination remain
available; editing, row mutation, tree mode, server-side row models, and aggregate footers do not.
Set showSidebar: true to expose a Pivot tab that changes dimensions, values, and built-in
aggregates directly from the table. The tab also repeats the generated-column visibility selector
so pivot configuration and display choices can be managed in one place.
Persist the complete JSON-safe state with the provider or component API:
const settings = provider.saveSettings(); // AgridSettings
await backend.save(settings);
provider.loadSettings(await backend.load());
The snapshot includes pivotConfig plus AgridControlState (visibility, widths, order, pinning,
filters, sorting, pagination, and aggregates). Sidebar pivot and visibility changes also emit the
full object through (settingsChange). Custom aggregate functions are intentionally rejected
because functions cannot be serialized safely.
Page selector
AgridPageSelectorComponent navigates a labeled list using previous/next buttons, a typed ID,
or a dropdown. Every navigation path emits the complete selected item through (selectPage).
readonly pages: AgridPageItem<number>[] = Array.from({ length: 100 }, (_, index) => ({
id: index + 1,
label: `Page ${index + 1}`,
}));
readonly selectedPageId = signal(1);
selectPage(item: AgridPageItem<number>): void {
this.selectedPageId.set(item.id);
}
<agrid-page-selector [items]="pages" [selectedId]="selectedPageId()"
(selectPage)="selectPage($event)" />
IDs may be strings or numbers. Type an exact ID and press Enter to jump. The component uses the
shared --agrid-color-* theme variables and accepts optional labels plus a disabled state.
Menu bar
Configure menuBarItems to render commands above the column headers. Main buttons and dropdown
items share the single (menuBarAction) output. visible, active, and disabled accept either
a boolean or a resolver receiving current rows, selected rows, selected cell, provider, and
datasource.
readonly provider = new AgridProvider<Order>({
columns,
datasource,
rowSelection: 'multi',
menuBarItems: [
{ id: 'refresh', label: 'Refresh', icon: '↻' },
{
id: 'selection',
label: 'Selection',
disabled: ({ selectedRows }) => selectedRows.length === 0,
active: ({ selectedRows }) => selectedRows.length > 0,
items: [
{ id: 'approve', label: 'Approve', visible: ({ selectedRows }) => selectedRows.length > 0 },
{ id: 'archive', label: 'Archive', disabled: ({ selectedRows }) => selectedRows.some(({ row }) => row.locked) },
],
},
],
});
onMenuBarAction(id: string): void {
// refresh, selection, approve, archive, ...
}
<agrid [provider]="provider" (menuBarAction)="onMenuBarAction($event)" />
Dynamic Provider Options
Three options are WritableSignal properties on the provider instance — update them at runtime without recreating the provider:
| Signal | Type | Description |
|---|---|---|
control.loading | Signal<boolean> | Whether the loading overlay is visible. Change with setLoading(). |
control.readonly | Signal<boolean> | Whether readonly mode is active. Change with setReadonly(). |
control.autoAddRows | Signal<boolean> | Whether automatic row insertion is active. Change with setAutoAddRows(). |
control.clearChangedCells(rowIndex?, fields?) | method | Clears every changed-cell marker, one row, or selected fields in one row. |
control.indicate(rowIndex, color, durationMs?) | method | Flashes one original datasource row with a CSS color, then fades back over durationMs (default 1000). |
Example — toggle readonly in a host component:
readonly provider = new AgridProvider({ ..., readonly: true });
readonly isEditing = signal(false);
constructor() {
effect(() => this.provider.control.setReadonly(!this.isEditing()));
}
Example — server-side loading state:
async loadPage(page: number) {
this.provider.control.setLoading(true);
this.ds.setData(await fetchPage(page));
this.provider.control.setLoading(false);
}
Public Component Methods
Call these through viewChild(AgridComponent).
| Method | Description |
|---|---|
autosizeAllColumns() | Resizes every visible column to fit its header text and current row values. Call after setting data. |
expandGroups() | Expands every group when grouping is active. |
collapseGroups() | Collapses every group when grouping is active. |
toggleSidebar() | Opens or closes the column sidebar. |
openFind() | Opens the find panel and focuses the input. |
closeFind() | Closes the find panel. |
goToFindMatch(direction) | Moves to the next (1) or previous (-1) find match. |
deleteRow(originalIndex) | Removes a row and emits rowRemoved, after confirmation when confirmRowDelete is enabled. |
clearChangedCells(originalIndex?, fields?) | Backwards-compatible delegate to provider.control.clearChangedCells(...). |
clearMarkedRows() | Clears all rows marked for clipboard inclusion. |
setRowMarked(index, marked) | Sets one row's mark state and emits rowMark when it changes. |
toggleRowMarked(index) | Toggles one row's mark state and emits rowMark. |
setColumnMarked(field, marked) | Sets one complete column's mark state and emits columnMark when it changes. |
toggleColumnMarked(field) | Toggles one complete column's mark state. |
clearMarkedColumns() | Clears all marked columns. |
Data Export
Export lives on the provider, so you can trigger it from anywhere that holds the provider — no viewChild(AgridComponent) required:
provider.exportCsv(); // downloads "export.csv"
provider.exportCsv('employees.csv');
provider.exportXlsx(); // downloads "export.xlsx"
provider.exportXlsx('employees.xlsx');
Both use display values (value-list labels, formatters) and respect column visibility; group-header rows are excluded. exportXlsx writes a real .xlsx workbook with zero third-party dependencies — numbers and dates are emitted as native, sortable/summable cells under a bold frozen header row. Both methods operate on the grid's current filtered, visible projection and are a no-op until an <agrid> bound to the provider has rendered.
Public Component State
| Property | Type | Description |
|---|---|---|
selectedCell | Signal<CellPosition | null> | Currently focused cell. |
editingCell | Signal<CellPosition | null> | Cell currently in edit mode. |
selectedRowIndices | Signal<ReadonlySet<number>> | Selected original row indices. |
selectedRowIndex | Signal<number | null> | First selected row index, useful for single selection. |
getCurrentRow() | AgridCurrentRow<T> | null | Returns the first selected row with its original index. |
getCurrentCell() | AgridCurrentCell<T> | null | Returns the selected cell with row, field, value, and column metadata. |
markedRowIndices | Signal<ReadonlySet<number>> | Original datasource indices included in copy operations. |
markedColumnFields | Signal<ReadonlySet<string>> | Fields currently marked as complete columns. |
selectionSummary | Signal<AgridSelectionSummary | null> | Live numeric statistics for the active cell or range. null when no numeric values are selected. |
sidebarOpen | Signal<boolean> | Current sidebar visibility. |
canUndo | Signal<boolean> | Whether Ctrl/Cmd+Z can undo an edit. Requires provider.control. |
canRedo | Signal<boolean> | Whether redo is available. Requires provider.control. |
filteredRowCount | Signal<number> | Total filtered data row count, unaffected by current page. |
totalPages | Signal<number> | Total page count given the current filter and page size. 1 when pagination is off. |
showPagination | Signal<boolean> | Whether the pagination bar is visible (pageSize > 0). |
Column Definitions
ColDef describes one column.
Columns, providers, datasources, and row events accept a row type. Supplying it makes column fields and callback values type-safe:
interface PersonRow {
id: number;
name: string;
active: boolean;
}
const columns: ColDef<PersonRow>[] = [
{ field: 'id', header: 'ID', formatter: value => value.toFixed(0) },
{ field: 'name', header: 'Name', formatter: value => value.toUpperCase() },
{
field: 'active',
header: 'Active',
values: [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' },
],
},
];
const datasource = new AgridDataSource<PersonRow>([]);
const provider = new AgridProvider<PersonRow>({ columns, datasource });
function onRecordEdit(event: RecordEditEvent<PersonRow>): void {
console.log(event.data.name);
}
An invalid field such as { field: 'email' } is rejected by TypeScript. Generic parameters
are optional, so existing untyped configurations remain compatible.
Grouped Column Headers
const columns: ColDef<PersonRow>[] = [
{ field: 'firstName', header: 'First name', group: 'employee' },
{ field: 'lastName', header: 'Last name', group: 'employee' },
{ field: 'email', header: 'Email' },
];
const provider = new AgridProvider({
columns,
headerGroups: [{ id: 'employee', label: 'Employee' }],
});
The extra header row appears when a visible column references a configured group. Only adjacent
columns share one group header. Reordering, hiding, or pinning columns can split the same group ID
into multiple rendered segments. Dragging a group header moves every column in that segment as one
ordered block. A segment containing a locked column cannot be dragged. The group property is only
for header presentation; groupable continues to control data-row grouping.
interface ColDef {
field: string;
header: string;
group?: string; // references AgridProvider.headerGroups
headerMenuItems?: AgridColumnHeaderMenuItem[];
width: number; // use ColDefAutoSize (-1) to autosize on first render
type?: 'text' | 'number' | 'date' | 'boolean';
editable?: boolean;
cellReadonly?: (params: { value: unknown; row: Record<string, unknown>; column: ColDef; originalIndex: number }) => boolean;
textAlign?: 'left' | 'center' | 'right' | Signal<'left' | 'center' | 'right'>;
cellFormat?: (params: { value: unknown; row: Record<string, unknown>; column: ColDef; originalIndex: number }) => CellFormat | null | undefined;
colSpan?: number | ((params: { value: unknown; row: Record<string, unknown>; column: ColDef; originalIndex: number }) => number);
locked?: boolean;
values?: string[] | ValueOption[];
formatter?: (value: unknown) => string;
inputMask?: (params: { value: unknown; row: Record<string, unknown>; column: ColDef }) => RegExp | null;
filterable?: boolean;
groupable?: boolean;
hidden?: boolean;
pinned?: 'left' | 'right';
aggregate?: 'sum' | 'avg' | 'min' | 'max' | 'count';
cellRenderer?: (params: { value: unknown; row: Record<string, unknown> }) => string;
cellClass?: (params: { value: unknown; row: Record<string, unknown> }) => string;
infoIcon?: boolean | ((params: { value: unknown; row: Record<string, unknown> }) => boolean);
}
| Property | Required | Description |
|---|---|---|
field | Yes | Key in each row object. |
header | Yes | Header label shown in the grid. |
group | No | Header-group ID. Adjacent columns with the same ID share a grouped header. |
headerMenuItems | No | Custom { key, label, icon?, disabled?, itemClasses?, iconClasses? } commands appended to this column's header menu. |
width | Yes | Default width in pixels. Set to ColDefAutoSize (-1) to fit the column to its content on first render. |
type | No | Semantic type. number initializes blank rows with 0. date treats the ISO date prefix as a calendar date, with localized display formatting and a native inline editor. |
editable | No | Set to false for a read-only column. Defaults to editable. |
cellReadonly | No | Return true to make one cell read-only from its current row, value, column, and original row index. Applies to inline edit, boolean toggles, paste, fill, and sidebar edits. |
textAlign | No | Static value or Angular Signal containing 'left', 'center', or 'right'. A textAlign returned by cellFormat overrides it for that cell. |
cellFormat | No | Returns per-cell visual overrides from the current row context. Its textAlign takes precedence over the column-level textAlign. |
colSpan | No | Number of adjacent visible columns occupied by a cell, or a row-aware callback returning that number. Spans stop at pinned-pane boundaries. |
locked | No | Prevents the column from being hidden, reordered, or unpinned through the column menu. |
values | No | Fixed editor/filter values. Use string[] or { value, label }[]. |
formatter | No | Custom display formatter. Takes precedence over date auto-formatting. |
inputMask | No | Resolves a regular-expression input constraint for each string cell from its row, value, and column. Invalid proposed values are rejected. |
filterable | No | Enables text filter and value picker for the column. |
groupable | No | Enables "group by" in the column menu. |
hidden | No | Hides the column on first render. |
pinned | No | 'left' or 'right' to pin the column initially. Left-pinned columns render in a fixed pane before the scrollable area; right-pinned columns render in a fixed pane after it. |
aggregate | No | Shows an aggregate footer value: 'sum', 'avg', 'min', 'max', or 'count'. |
cellRendererComponent | No | A standalone Angular component rendered for the cell's display state. The component injects AGRID_RENDERER_CONTEXT. See Custom Cell Renderers. |
cellRenderer | No | Deprecated — use cellRendererComponent. Custom HTML renderer: return an HTML string; Angular sanitizes it automatically. |
cellEditor | No | A standalone Angular component to use as the cell editor instead of the built-in input. The component injects AGRID_EDITOR_CONTEXT. See Custom Cell Editors. |
cellClass | No | Returns a CSS class name for each cell. Applied alongside built-in state classes. |
infoIcon | No | Shows a right-aligned ? action. Set it to true or return a boolean per cell. Clicking it emits cellInfo with the row, field, value, original index, and column definition. |
Runtime text alignment
Use a host-owned writable signal when alignment must change at runtime. Pass that signal into the
column definition and update the original signal from columnHeaderAction:
import { signal } from '@angular/core';
import { ColDef, ColumnHeaderActionEvent } from '@thkl/agrid';
type TextAlign = 'left' | 'center' | 'right';
readonly salaryAlignment = signal<TextAlign>('right');
readonly columns: ColDef<Employee>[] = [
{
field: 'salary',
header: 'Salary',
textAlign: this.salaryAlignment,
headerMenuItems: [
{ key: 'align-left', label: 'Align left' },
{ key: 'align-center', label: 'Align center' },
{ key: 'align-right', label: 'Align right' },
],
},
];
onColumnHeaderAction(event: ColumnHeaderActionEvent<Employee>): void {
if (event.column.field !== 'salary') return;
const alignment = event.key.replace('align-', '') as TextAlign;
this.salaryAlignment.set(alignment);
}
<agrid [provider]="provider" (columnHeaderAction)="onColumnHeaderAction($event)" />
Do not assign a new string to event.column.textAlign for runtime changes. A column definition is
configuration rather than reactive state, so mutating the object does not notify rendered cells.
Also, event.column.textAlign is typed as a static value or a read-only Signal; retain the
original writable signal when calling .set().
Cell spanning
Use colSpan to merge adjacent cells horizontally for selected rows:
const columns: ColDef<OrderRow>[] = [
{
field: 'label',
header: 'Label',
textAlign: 'center',
colSpan: ({ row }) => row.kind === 'summary' ? 3 : 1,
},
{ field: 'quantity', header: 'Quantity' },
{ field: 'total', header: 'Total' },
];
The callback receives the typed row, value, column, and original datasource index. Values are rounded down and clamped to the columns remaining in the current pane. A span cannot cross from a left-pinned pane into the scrollable pane or from the scrollable pane into a right-pinned pane.
<agrid [provider]="provider" (cellInfo)="showCellInfo($event)" />
Input masks
Return a mask per row when string values need a structured format:
{
field: 'reference',
header: 'Reference',
inputMask: ({ row }) =>
row.numeric
? /\d{0,3}(?:-\d{0,5}(?:-\d{0,5})?)?/
: /[a-z0-9]{0,3}(?: [a-z0-9]{0,3}(?: [a-z0-9]{0,5})?)?/i,
}
The expression is matched against the entire proposed value, so explicit ^
and $ anchors are optional. It must accept partial input, including the empty
string and any intermediate separators users need to type. Return null when
a particular row should use an unrestricted text editor.
ColDefAutoSize
Import ColDefAutoSize and use it as the width value to fit the column to its content on first render:
import { ColDefAutoSize } from './agrid';
const columns: ColDef[] = [
{ field: 'name', header: 'Name', width: ColDefAutoSize },
{ field: 'email', header: 'Email', width: ColDefAutoSize },
];
The column sizes itself once on first render and then behaves like a normal resizable column.
Value Options
Use value options when stored values differ from labels.
interface ValueOption {
value: unknown;
label: string;
}
Example:
{
field: 'departmentId',
header: 'Department',
width: 140,
values: [
{ value: 1, label: 'Engineering' },
{ value: 2, label: 'Sales' },
],
}
The grid displays labels, but committed edits store value.
Date Auto-Formatting
The grid automatically detects and formats date values without any configuration. Both display and sorting use the native date value.
Auto-detected formats:
Dateobjects- ISO 8601 strings:
"2024-01-15","2024-01-15T10:30:00Z","2024-01-15T10:30:00+02:00"
Display: Values are formatted using the browser's locale — e.g. Jan 15, 2024.
Sorting: Date columns sort chronologically by raw timestamp, not alphabetically by display string.
Priority: values list → formatter → date auto-format → raw string.
To force date formatting on a column regardless of value shape, set type: 'date'.
To use a custom date format, set formatter:
{ field: 'hiredAt', header: 'Hired', width: 120,
formatter: v => new Date(v as string).toLocaleDateString('de-DE') }
Zebra Stripes
Alternating row shading is opt-in via the provider:
readonly provider = new AgridProvider({ ..., zebraStripes: true });
Override the stripe color with a CSS custom property on the host:
agrid {
--agrid-color-bg-stripe: #f0f4ff;
}
Hover and selection colors always override the stripe.
Readonly Mode
Set readonly: true in the provider to make the entire grid non-editable:
readonly provider = new AgridProvider({ ..., readonly: true });
To toggle readonly at runtime, update the control:
readonly isReadonly = signal(true);
constructor() {
effect(() => this.provider.control.setReadonly(this.isReadonly()));
}
Individual ColDef.editable: false still works when readonly is false.
Pagination
Pagination is controlled through AgridControl. When a page size is set the grid renders a page bar at the bottom showing « ‹ page / total › » and the total filtered row count.
readonly gridControl = new AgridControl({ pageSize: 25 });
Or change it at runtime:
this.gridControl.setPageSize(10); // 0 = show all rows
this.gridControl.setPage(2);
Pagination applies to data rows after filtering and sorting, before grouping. Each page therefore always contains at most pageSize data rows.
Server-side filtering and sorting
Enable serverSideFiltering when the API should filter and sort the dataset:
readonly provider = new AgridProvider({
columns: [
{ field: 'name', header: 'Name', filterable: true },
{ field: 'status', header: 'Status', filterable: true },
],
datasource: this.ds,
control: this.ctrl,
serverSideFiltering: true,
sortOption: 'single',
});
<agrid
[provider]="provider"
(serverQueryChange)="loadRows($event)"
/>
loadRows(query: AgridServerQuery): void {
this.api.orders(query).subscribe(result => {
this.ds.setData(result.rows);
this.ctrl.setTotalRows(result.total);
});
}
For signal-backed stores, subscribe to the provider instead of template outputs:
effect(() => {
const query = this.provider.serverQuery();
if (!query) return;
this.store.load(query);
});
In server-side mode:
- Filter and sort state remains visible in the grid headers.
- The grid does not filter or sort loaded rows locally.
- The distinct-value checklist is hidden unless
ColDef.valuessupplies the complete server-side value set. AgridServerQuerycontains column filters, value selections, menu conditions, ordered sorts, quick-filter text, and page range.- Clearing emits an empty filter value,
selectedValues: null, or anullsort direction on the compatibility outputs. - Multi-column sorting emits one event for each changed column.
- Text filter events are debounced by
filterDebounceMs(300 ms by default).
Use sortOption: 'single' for backends that accept only one sort field. Selecting another column
clears the previous sort first. Use 'none' to remove sorting controls completely; 'multi'
preserves the default multi-column behavior.
The grid updates its visible filter state immediately, but only emits the final server text or
condition value after the debounce delay. Set filterDebounceMs: 0 when immediate events are
required. For server pagination, set a pageSize, call control.setTotalRows(total) after each
response, and replace the datasource contents with the returned page.
Server-side row model
Use AgridServerSideRowModel when rows should be loaded in blocks as the virtual viewport scrolls.
Global row indices remain stable, unloaded rows render as placeholders, stale responses are ignored
after query changes, and old blocks are evicted at the configured cache limit.
const rowModel = new AgridServerSideRowModel<Order>({
blockSize: 100,
maxBlocksInCache: 8,
initialRowCount: 1_000_000, // optional; the server can return rowCount instead
datasource: {
async getRows(request) {
const response = await api.searchOrders(request);
return { rows: response.rows, rowCount: response.total };
},
},
});
const provider = new AgridProvider<Order>({
columns,
serverSideRowModel: rowModel,
enableQuickFilter: true,
});
Requests contain the half-open startRow/endRow range, complete column filter state, ordered sort
entries, and the quick-filter string. Returning rowCount sets the exact scrollbar extent. Without
it, a short block marks the end and a full block extends the unknown extent by one block.
The initial row model is flat: client-side grouping, tree data, pinned rows, master/detail, pagination, and local aggregate footers are not applied. Editing updates the loaded cache; persist edits from grid events because an evicted block is fetched again.
Custom Cell Renderers
Point ColDef.cellRendererComponent at any standalone Angular component to control the cell's
display (read) state. The component injects AGRID_RENDERER_CONTEXT to read the value, row, and
column as signals — with full Angular bindings, event handlers, and child components, and no
manual HTML escaping or sanitization. This needs no third-party dependency.
import { AGRID_RENDERER_CONTEXT } from '@thkl/agrid';
@Component({
selector: 'status-badge',
template: `<span class="badge" [class]="'badge--' + value()">{{ value() }}</span>`,
})
export class StatusBadge {
private readonly ctx = inject(AGRID_RENDERER_CONTEXT);
readonly value = computed(() => String(this.ctx.value() ?? ''));
}
@Component({
selector: 'score-bar',
template: `<span class="track"><span class="fill" [style.width.%]="value()"></span></span>`,
})
export class ScoreBar {
private readonly ctx = inject(AGRID_RENDERER_CONTEXT);
readonly value = computed(() => Number(this.ctx.value() ?? 0));
}
const columns: ColDef[] = [
{ field: 'status', header: 'Status', editable: false, cellRendererComponent: StatusBadge },
{ field: 'score', header: 'Performance', editable: false, cellRendererComponent: ScoreBar },
];
AgridRendererContext
| Member | Type | Description |
|---|---|---|
value | Signal<T> | The cell's current value. |
row | Signal<Record<string, unknown>> | The full row record — useful when the display depends on sibling fields. |
column | Signal<ColDef> | The column definition being rendered. |
A component renderer also works for boolean columns, replacing the default checkbox. See the
Custom cells demo for badge and score-bar renderers.
Deprecated: HTML-string cellRenderer
Deprecated. Prefer
cellRendererComponent. The string renderer remains supported for now but will be removed in a future release.
Return an HTML string from cellRenderer to render content in a cell. Angular's built-in
sanitization runs automatically. Use CSS classes rather than inline styles, and escape dynamic text
before interpolating it into HTML.
const columns: ColDef[] = [
{
field: 'status',
header: 'Status',
editable: false,
cellRenderer: ({ value }) =>
`<span class="status-badge status-badge--${value}">${value}</span>`,
},
];
The row parameter gives you access to the full row object. When both are set,
cellRendererComponent wins.
Custom Cell Editors
When the built-in text input, dropdown, or checkbox isn't enough, point ColDef.cellEditor at any
standalone Angular component. The grid instantiates it while the cell is in edit mode and provides
an AgridEditorContext through dependency injection. The editor is purely an input surface — the
grid keeps ownership of validation (validate), undo/redo history, and the commit/cancel lifecycle,
so Tab, Enter, and Escape keep working without any extra wiring (their key events bubble up to
the grid). This needs no third-party dependency — just Angular.
Inject AGRID_EDITOR_CONTEXT to talk to the grid:
import { AGRID_EDITOR_CONTEXT } from '@thkl/agrid';
@Component({
selector: 'star-rating-editor',
template: `
@for (n of [1, 2, 3, 4, 5]; track n) {
<button type="button" (click)="pick(n)">{{ n <= value() ? '★' : '☆' }}</button>
}`,
})
export class StarRatingEditor {
private readonly ctx = inject(AGRID_EDITOR_CONTEXT);
readonly value = signal(Number(this.ctx.value() ?? 0));
pick(n: number): void {
this.value.set(n);
this.ctx.setDraft(n); // stage the value the grid will commit
this.ctx.commit(); // confirm immediately (optional — Tab/Enter also commit)
}
}
const columns: ColDef[] = [
{
field: 'rating',
header: 'Rating',
type: 'number',
cellEditor: StarRatingEditor,
cellRenderer: ({ value }) => '★'.repeat(Number(value)), // how it looks when not editing
},
];
AgridEditorContext
| Member | Type | Description |
|---|---|---|
value | Signal<T> | The cell's value when editing started. |
row | Signal<Record<string, unknown>> | The full row record. |
column | Signal<ColDef> | The column definition being edited. |
seedChar | Signal<string> | The printable character that triggered type-to-edit, or ''. Seed a free-text editor with it. |
setDraft(value) | method | Stage a value; the grid commits the last staged value on Tab/Enter. |
commit() | method | Commit the staged value programmatically (same as pressing Enter). |
cancel() | method | Discard the edit (same as pressing Escape). |
Pair cellEditor with cellRenderer to control how the value looks when the cell isn't being
edited. See the Custom editors demo for star-rating, colour-swatch, and slider editors.
Charts / Graphs
AgridChartComponent renders zero-dependency SVG charts/graphs — column, bar, line, area, pie,
and donut — configured exactly like the grid: build an AgridChartProvider and pass it in.
import { AgridChartComponent, AgridChartProvider } from '@thkl/agrid';
readonly chartProvider = new AgridChartProvider({
type: 'column',
data: {
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
series: [
{ name: 'North', values: [120, 145, 138, 162] },
{ name: 'South', values: [98, 110, 134, 128] },
],
},
height: 300,
});
<agrid-chart [provider]="chartProvider" />
The chart sizes itself to its container width (observed) and the provider's height. Every option
is a signal: type is writable, so chartProvider.type.set('pie') re-renders.
AgridChartProvider configuration
| Option | Type | Default | Description |
|---|---|---|---|
type | 'column' | 'bar' | 'line' | 'area' | 'pie' | 'donut' | — | Diagram type. Writable signal — set it to switch at runtime. |
data | AgridChartData | — | Static dataset ({ categories?, series: [{ name?, color?, values }] }). Ignored when source is set. |
source | Signal<readonly T[]> | — | A reactive row source to derive the dataset from. Requires transform. See below. |
transform | (rows, type) => AgridChartData | — | Turns the source rows (and current type) into a dataset. Re-runs when the rows or type change. |
height | number | 220 | Chart height in pixels (width follows the host). |
showLegend | boolean | true | Show the series/category legend. |
showAxis | boolean | true | Draw value/category axes (cartesian types). |
palette | string[] | built-in | Override the series/slice colours. |
Pie and donut charts use the first series; its values become slices labelled by categories.
Linking a chart to a grid
Pass a grid provider's visibleRows as the source and the chart follows the grid live — including
its filters and sorting:
readonly chartProvider = new AgridChartProvider<RegionRow>({
type: 'column',
source: this.gridProvider.visibleRows, // filtered + sorted rows
transform: (rows, type) => ({
categories: rows.map(r => r.region),
series: [{ values: rows.map(r => r.total) }],
}),
});
AgridProvider.visibleRows is a Signal<readonly T[]> of the grid's current rows. Important
semantics:
- It reflects filtering and sorting — filter a row out of the grid and it leaves the chart too.
- It deliberately ignores grouping and pagination: a chart wants the whole filtered set, not a
single page or grouped subtotals. (Use a
transformto aggregate if you want grouped totals.) - It is published by the rendered grid component. Before the grid mounts (or with no grid attached) it falls back to every datasource row.
With a source the dataset is a derived signal, so setData() is unavailable — update the source or
the transform instead. Without a source, use chartProvider.setData(next) to replace static data.
See the Charts demo for a grid beside a chart with a type switcher. The main demo also has a Show Charts menu action that opens a graph panel linked to the grid's filtered rows. In both cases, filtering a row or editing a cell redraws the chart instantly.
Column Autosize
Fit all visible columns to their content after loading data:
constructor() {
afterNextRender(() => this._grid()?.autosizeAllColumns());
}
Or autosize a single column by double-clicking its resize handle, or through the column menu.
AgridDataSource
AgridDataSource<T> is a signal-based row container shared by the host and grid.
const ds = new AgridDataSource<Record<string, unknown>>([
{ id: 1, name: 'Alice' },
]);
Linking an Angular signal
Link a writable Angular signal directly when the application and grid should share ownership of the rows:
interface Row {
id: number;
name: string;
}
readonly rows = signal<Row[]>([
{ id: 1, name: 'Alice' },
]);
readonly ds = new AgridDataSource<Row>();
readonly provider = new AgridProvider({
columns: [
{ field: 'id', header: 'ID', editable: false },
{ field: 'name', header: 'Name' },
],
datasource: this.ds,
});
constructor() {
this.ds.linkSignal(this.rows);
}
No synchronization effect() is needed. Updates work in both directions:
- Calling
rows.set(...)orrows.update(...)refreshes the grid. - Cell edits, paste,
setData,updateRow,patchRow,addRow,removeRow, andmoveRowupdaterowsautomatically. - Undo and redo also update
rowsbecause they use datasource mutations.
The (cellEdit) output is not required to keep the writable signal synchronized. Use it only for
side effects such as saving changes to an API:
<agrid [provider]="provider" (cellEdit)="saveEdit($event)" />
For one-way linking, pass a readonly signal:
readonly rows = signal<Row[]>([]);
constructor() {
this.ds.linkSignal(this.rows.asReadonly());
}
In this mode, source updates refresh the grid, but grid mutations remain local to the datasource. In both modes, source updates are linked without copying the source array.
| Member | Description |
|---|---|
rows | Readonly Angular Signal<T[]> of current rows. |
linkSignal(source) | Links an external signal without copying. Writable signals receive datasource mutations automatically. |
setData(rows) | Replaces all rows with a shallow copy. |
updateRow(index, row) | Replaces one row. |
patchRow(index, patch) | Merges a partial row update. |
addRow(row, atIndex?) | Inserts a row and returns the inserted index. |
removeRow(index) | Removes a row. |
moveRow(from, to) | Moves a row using insert-before semantics. |
getRow(index) | Returns a non-reactive row snapshot. |
length | Current row count. |
AgridControl
AgridControl stores optional grid UI state and behavior. Assign it to AgridProvider.control to enable persisted state, filters, sort, grouping, visibility, pinning, row reorder, pagination, and undo/redo.
const control = new AgridControl({
allowRowReorder: true,
hiddenColumns: ['salary'],
pinnedColumns: ['id'],
pageSize: 20,
});
Control State
interface AgridControlState {
columnWidths: Record<string, number>;
filters: Record<string, ColumnFilter>;
allowRowReorder?: boolean;
groupByField?: string | null;
hiddenColumns?: string[];
columnOrder?: string[];
pinnedColumns?: string[];
pageSize?: number;
currentPage?: number;
}
Column Filters
The header arrow opens the complete column menu. The condition button beside an inline filter opens only condition operators and operands. Each trigger toggles its own menu mode.
interface ColumnFilter {
text: string;
selectedValues: string[] | null;
sort: 'asc' | 'desc' | null;
}
text, selectedValues, and sort are combined when rows are displayed. selectedValues: null means all values are allowed.
Control Signals
| Signal | Description |
|---|---|
loading | Whether the loading overlay is visible. Change with setLoading(). |
readonly | Whether all editing and mutation UI is disabled. Change with setReadonly(). |
autoAddRows | Whether navigation can insert rows automatically. Change with setAutoAddRows(). |
allowRowReorder | Whether row drag handles can reorder rows. |
groupByField | Field currently used for grouping, or null. |
hiddenColumns | Set of hidden field names. |
columnOrder | Current field order. Empty means original colDefs order. |
pinnedColumns | Set of pinned field names. |
columnWidths | Width overrides by field. |
filters | Active filter/sort state by field. |
pageSize | Rows per page. 0 means all rows (no pagination). |
currentPage | Current page number (1-based). |
filterReapplyNeeded | Whether inserted rows are currently bypassing active filters or sorts. |
canUndo | Whether an undo history item exists. |
canRedo | Whether a redo history item exists. |
Control Methods
| Method | Description |
|---|---|
setLoading(value) | Shows or hides the loading overlay. |
setReadonly(value) | Enables or disables readonly mode. |
setAutoAddRows(value) | Enables or disables automatic row insertion. |
setAllowRowReorder(value) | Enables or disables row reorder. |
setGroupBy(field) | Groups by a field or clears grouping with null. |
isColumnHidden(field) | Returns whether a column is hidden. |
setColumnVisibility(field, visible) | Shows or hides a column. |
toggleColumnVisibility(field) | Toggles column visibility. |
setColumnOrder(fields) | Replaces the current column order. |
moveColumn(currentVisibleOrder, fromField, toField, insertBefore) | Reorders columns. Used by header dragging. |
isPinned(field) | Returns whether a column is pinned. |
setPinned(field, pinned) | Pins or unpins a column. |
togglePinned(field) | Toggles pinning. |
getColumnWidth(field, defaultWidth) | Returns effective width. |
setColumnWidth(field, width) | Sets a width override with a 40 px minimum. |
getFilter(field) | Returns current filter state or defaults. |
getFilterModel() | Returns a detached serializable filter, quick-filter, and sort snapshot. |
setFilterModel(model) | Replaces filters, quick filter, and sort order. Pass null to clear them. |
setTextFilter(field, text) | Sets text filter. |
setSelectedValues(field, values) | Sets allowed values, or null for all. |
setSort(field, sort) | Sets sort and clears sort on other fields. |
reapplyFilters() | Applies active filters and sorts to rows inserted since the last explicit reapply. |
clearFilter(field) | Clears one column filter/sort. |
clearAllFilters() | Clears all filters and sorts. |
hasActiveFilter(field) | Returns whether a column has active filter/sort state. |
hasAnyActiveFilter() | Returns whether any column has active filter/sort state. |
setPageSize(size) | Sets rows per page. 0 disables pagination. Resets to page 1. |
setPage(page) | Navigates to a page (1-based). Clamped to valid range by the grid. |
pushEdit(entry) | Adds one edit to undo history. Used by the grid. |
pushEditBatch(entries) | Adds a multi-cell operation as one undo step. Used by paste/fill. |
undo() | Returns a HistoryItem to reverse, or null. The grid applies it. |
redo() | Returns a HistoryItem to reapply, or null. The grid applies it. |
clearHistory() | Clears undo/redo history. |
toJSON() | Serializes control state including pagination. |
AgridControl.fromJSON(state) | Restores control state. |
Events And Types
FirstDataRenderedEvent
firstDataRendered fires once per grid component, after the first completed render that contains
real datasource rows. If the datasource starts empty, the event waits until rows are supplied.
Server-side loading placeholders do not trigger it.
<agrid [provider]="provider" (firstDataRendered)="onGridReady($event)" />
interface FirstDataRenderedEvent<T> {
rows: readonly T[];
rowCount: number;
provider: AgridProvider<T>;
datasource: AgridDataSource<T>;
}
GridEditEvent
interface GridEditEvent {
position: CellPosition;
field: string;
oldValue: unknown;
newValue: unknown;
}
Emitted whenever a committed grid operation changes a cell.
FilterChangeEvent
interface FilterChangeEvent {
field: string;
value: string;
selectedValues?: readonly string[] | null;
operator?: FilterOperator | null;
operand?: string | null;
operand2?: string | null;
}
An empty value clears the server-side text filter. selectedValues: null clears a value-list
filter. Condition filters include operator, operand, and operand2.
AgridServerQuery
interface AgridServerQuery {
filters: Readonly<Record<string, ColumnFilter>>;
sort: readonly { field: string; direction: 'asc' | 'desc' }[];
quickFilter: string;
page: number;
pageSize: number;
startRow: number;
endRow: number; // inclusive
}
Emitted by (serverQueryChange) and published as provider.serverQuery.
SortChangeEvent
interface SortChangeEvent {
field: string;
direction: 'asc' | 'desc' | null;
}
A null direction clears the server-side sort for that field.
CellPosition
interface CellPosition {
rowIndex: number;
colIndex: number;
}
rowIndex is the original data-source row index. colIndex is the visible column index.
RowSelectEvent
interface RowSelectEvent {
rows: { row: Record<string, unknown>; originalIndex: number }[];
}
rowSelect emits null when selection is cleared.
CellSelectEvent
interface CellSelectEvent<T> {
position: CellPosition;
row: T;
originalIndex: number;
field: keyof T;
value: T[keyof T];
column: ColDef<T>;
}
cellSelect emits null when selection is cleared. Use grid.getCurrentCell() to read the same
shape on demand.
RowReorderEvent
interface RowReorderEvent {
row: Record<string, unknown>;
oldIndex: number;
newIndex: number;
}
The grid does not reorder rows itself on drop. Call dataSource.moveRow(event.oldIndex, event.newIndex) in the handler.
NewRecord
interface NewRecord {
index: number;
data: Record<string, unknown>;
provider: AgridProvider;
datasource: AgridDataSource;
}
For repeated grids, use the source carried by the event instead of looking the provider up by the row or loop index:
onPrepareAdd(event: NewRecord): void {
const next = event.datasource.length;
event.datasource.patchRow(event.index, { id: next, departmentId: 1 });
}
Emitted after the grid inserts a blank row. Patch defaults from the host if needed.
RowUpdateEvent
interface RowUpdateEvent<T extends object = Record<string, unknown>> {
row: T;
originalIndex: number;
}
rowChanged carries the latest complete datasource row and its current zero-based index. Inline
edits are grouped until the active row is left. Sidebar-only editing emits the same event when the
Save button is clicked.
GroupAction
interface GroupAction {
label: string;
action: (groupLabel: string) => void;
}
Actions appear in group header menus.
HistoryEntry And HistoryItem
interface HistoryEntry {
rowIndex: number;
field: string;
oldValue: unknown;
newValue: unknown;
}
type HistoryItem = HistoryEntry | HistoryEntry[];
Paste and fill store multiple entries as one HistoryItem, so Ctrl/Cmd+Z reverses the whole operation.
Keyboard And Mouse Behavior
| Action | Behavior |
|---|---|
| Arrow keys | Move active cell. |
| Shift+arrow | Extend cell range selection. |
| Page Up / Page Down | Move by the number of complete rows visible in the viewport. |
| Home / End (Pos1 / Ende) | Move to the first / last cell in the current row. |
| Ctrl/Cmd+Home / Ctrl/Cmd+End | Move to the first / last cell in the projected grid. |
| Tab / Shift+Tab | Move right / left, wrapping rows. |
| Enter | Start editing the active cell. |
| Enter while editing | Commit and follow enterEditAction (nextRow by default). |
| Ctrl/Cmd+Enter | Toggle an expandable tree node. |
| F2 | Start editing active cell. |
| Printable key | Start editing active cell with typed seed character. |
| Escape | Close any open menu, cancel edit, or close find when its input is focused. |
| Ctrl/Cmd+Z | Undo. |
| Ctrl/Cmd+Y | Redo. |
| Ctrl/Cmd+Shift+Z | Redo. |
| Ctrl/Cmd+F | Open find panel. |
| Enter in find | Next match. |
| Shift+Enter in find | Previous match. |
Opening find clears the active cell so typing remains in the find input. Tree searches include
collapsed descendants; navigating to one expands its ancestor path before scrolling to the match.
| Click cell | Select cell. |
| Shift+click cell | Extend range selection. |
| Drag across cells | Select a rectangular range; dragging beyond the viewport auto-scrolls. |
| Double-click cell | Start editing. |
| Drag fill handle | Fill selected value/range down or right. |
| Double-click resize handle | Autosize column. |
| Drag resize handle | Resize column. |
| Drag header | Reorder columns when control is provided. |
| Right-click control cell | Open row context menu. |
Filtering, Sorting, And Grouping
- A filter row appears when at least one visible column has
filterable: true. - Text filter and value picker are combined.
- Sort is single-column. Setting sort on one field clears sort on other fields.
- Date columns sort chronologically by raw value, not alphabetically by display string.
- Grouping is enabled per column with
groupable: true. - Group state is controlled through
AgridControl.setGroupBy(field | null). expandGroups()andcollapseGroups()can be called on the component.
Clipboard, Range Selection, And Fill
- Copy exports the active cell or selected rectangular range as TSV.
- With
enableRowMarking, clicking a row header outside its nested controls or using its checkbox toggles the mark and emitsrowMark. - With
showRowNumbers, the control column displays 1-based row numbers for the current filtered and sorted row order instead of the drag-handle glyph. - Marked rows are appended to every copy using the copied columns.
- Copying without an active cell copies all visible columns from the marked rows.
- Context-menu
Copy cellandCopy rowalso include marked rows without duplicates.
Selection status bar
Selecting a numeric cell or rectangular range shows a status bar beneath the grid body with Count, Sum, Average, Minimum, and Maximum. It updates immediately when the range, projected rows, columns, or datasource values change and hides when the selection contains no numeric values.
Actual finite numbers are included in every column. Numeric strings are included only when their
column declares type: 'number', preventing IDs and numeric-looking text from being aggregated by
accident. Group headers, detail panels, loading rows, blanks, NaN, and infinite values are skipped.
The raw values are also available programmatically:
const summary = grid.selectionSummary();
// { count, sum, average, min, max } | null
The dedicated /selection-summary demo shows Shift+click and drag selection across comparable
regional revenue columns and mirrors the raw signal above the grid.
- Row marking is independent from row selection.
- Marked rows remain part of copy output when filters hide them.
- Paste accepts TSV or CSV-like plain text and writes from the active cell.
- Pasted values use labels/raw values for
valuescolumns. - Number columns coerce numeric pasted values to
number. - Paste skips read-only columns.
- Fill repeats the selected source block into the dragged target area.
- Paste and fill are each one undo history item.
Master/Detail and Pinned Rows
Master/detail
Set masterDetail: true and provide a detailRenderer to make every data row expandable. A chevron
appears in the control column; clicking it reveals a detail panel rendered beneath the row. The
renderer returns an HTML string (sanitized automatically, like cellRenderer).
readonly provider = new AgridProvider<Order>({
columns, datasource,
masterDetail: true,
detailRowHeight: 160, // fixed panel height in px (default 200)
detailColumnField: 'notes',
detailActions: [
{ id: 'follow-up', label: 'Follow-up', text: '\nFollow-up required.' },
{ id: 'customer', label: 'Customer', text: ({ row }) => `\nCustomer: ${row.customer}` },
],
detailRenderer: ({ row }) => `<div class="order-detail">${row.notes}</div>`,
});
detailColumnField is optional. When set, the panel shows that column's formatted value below the
custom renderer. If the linked cell is editable, click or Enter opens a multiline textarea. Blur or
Ctrl/Cmd+Enter commits through normal validation, undo history, cellEdit, and recordEdit flows;
Escape cancels. detailActions can add text-template buttons above the textarea; buttons insert at
the current selection, or append when the editor is opened by the button. editable: false,
cellReadonly, and grid read-only mode are respected.
Detail panels are sized by a built-in variable-height virtual-scroll strategy, so large lists stay
performant whether or not panels are open. In tree mode, only leaf rows expose detail panels;
parent rows continue to control tree expansion. Master/detail remains disabled while grouping.
Toggle a panel imperatively with the public
toggleDetail(originalIndex) / isDetailExpanded(originalIndex) methods on the component.
Pinned rows
pinRow designates rows to keep fixed at the top or bottom of the body during vertical scroll —
ideal for header or total/summary rows. Pinned rows are pulled out of grouping and pagination but
keep their real data-source index, so editing, selection, and cell rendering work on them unchanged.
readonly provider = new AgridProvider<Order>({
columns, datasource, // datasource includes a summary row
pinRow: row => (row.isSummary ? 'bottom' : undefined),
});
Interactive pinning. Right-click any row (its cell context menu, or the control-cell row menu)
to Pin row to top / bottom or Unpin row. A runtime override always wins over the pinRow
predicate, so a user can unpin a declaratively-pinned row. Drive it programmatically with the
public component methods pinRowTo(originalIndex, 'top' | 'bottom' | null) and
rowPinState(originalIndex).
Pinned rows are designated over existing data-source rows (not a separate detached array). Keyboard arrow-navigation and range-selection do not currently cross the body↔pinned boundary.
Row CSS classes
getRowClass returns class names for a whole data row, complementing the per-cell ColDef.cellClass:
getRowClass: ({ row }) => (row.status === 'overdue' ? 'row-danger' : '')
Pinned Columns
Pinned columns are rendered in a fixed left pane. The unpinned columns render in a separate horizontally scrollable pane. Vertical scrolling is synchronized between the panes.
Pin columns initially with:
{ field: 'id', header: 'ID', width: 70, pinned: 'left' }
Or at runtime:
control.setPinned('id', true);
State Persistence
const saved = localStorage.getItem('agrid-state');
const control = AgridControl.fromJSON(saved ? JSON.parse(saved) : {});
localStorage.setItem('agrid-state', JSON.stringify(control.toJSON()));
Persisted state includes widths, filters, sort, grouping, hidden columns, column order, pinned columns, row reorder setting, page size, and current page.
Layout In A Card Or Flex Container
The grid host is a flex column. Give it a defined height by participating in the parent's flex layout:
/* Angular Material card example */
mat-card {
height: 600px;
display: flex;
flex-direction: column;
}
mat-card-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding: 0;
}
agrid {
flex: 1;
min-height: 0;
}
CSS Custom Properties
Override these on the agrid host element to theme the grid.
| Property | Default | Description |
|---|---|---|
--agrid-color-text | #24292f | Primary text color. Also used by agrid-tree. |
--agrid-color-text-muted | #57606a | Secondary / placeholder text. Also used by agrid-tree. |
--agrid-color-accent | #1a73e8 | Selection, focus, and active state color. |
--agrid-color-border | #d0d7de | Cell and header borders. |
--agrid-color-bg | #ffffff | Cell background. |
--agrid-color-bg-subtle | #fafbfc | Control column background. |
--agrid-color-bg-muted | #f6f8fa | Header and hover background. |
--agrid-color-bg-stripe | #f0f2f5 | Zebra stripe background (even rows). |
--agrid-color-cell-changed | #f59e0b | Corner marker for changed cells. |
--agrid-color-row-marked | #fff8c5 | Background for rows marked for clipboard inclusion. |
--agrid-color-column-marked | #e8f0fe | Background for marked columns. |
Development
pnpm install
pnpm start
pnpm build # publishable package
pnpm build:demo
pnpm copy:local # uncompiled runtime sources in localdist/agrid
pnpm test
pnpm test:e2e
pnpm test:performance
The TypeScript compile check:
./node_modules/.bin/tsc --noEmit -p tsconfig.app.json
The Playwright suite starts the Angular demo server automatically and runs the grid interaction tests in Chromium. Install its browser once when setting up a new environment:
pnpm exec playwright install chromium
pnpm test:performance runs the isolated large-dataset suite serially against 10k, 50k, 100k,
and 250k rows. It reports initial render, filtering, sorting, grouping, aggregation, row updates,
and virtual-scroll timings without enforcing machine-dependent thresholds. The same operations can
be run manually at /performance.
pnpm build:lib increments the package patch version and creates the publishable Angular package
in dist/agrid-package. Inspect the package
contents with:
cd dist/agrid-package
npm pack --dry-run
pnpm copy:local recreates localdist/agrid with only the library's runtime .ts, .html, and
.css files. Tests, documentation, licenses, package metadata, and build configuration are
excluded, making the directory suitable for source-level debugging in another Angular workspace.