cats-data-grid
v2.1.8
Published
Cats Data Grid is an Angular library for displaying tabular and hierarchical data. It ships two standalone components:
Readme
Cats Data Grid
Cats Data Grid is an Angular library for displaying tabular and hierarchical data. It ships two standalone components:
CatsDataGridComponent(<cats-data-grid>) — flat / grouped tables.CommonTreeTableComponent(<cats-tree-table>) — tree / parent-child tables.
It supports sorting (including a secondary sort key), text/number/date/set filtering, client and server side pagination, infinite scroll, single and multiple row selection, whole-cell selection, column settings (show/hide, reorder), column pinning, row grouping, inline cell editing (input or textarea), skeleton loading, custom cell renderers, custom row classes and a custom no-data template.
Table Of Contents
- Requirements
- Install
- Configure Assets And Styles
- Quick Start
- Column Definition (
colDefs) - Integrated Charts
- Data Grid Inputs
- Data Grid Events
- Master / Detail Rows
- Built-In Cell Renderers
- Identity Cell Renderers
- Numeric Cell Renderers
- Chart Cell Renderers
- Sorting And Secondary Sort
- Filtering
- Pagination And Infinite Scroll
- Row Selection
- Cell Selection
- Inline Cell Editing (Input / Textarea)
- Row Grouping
- Column Pinning
- Column Settings Panel
- Row Numbers
- Footer Totals
- Table Appearance
- Custom Row Classes
- Conditional Behavior
- Custom No-Data Template
- Skeleton Loading
- Server-Side Data
- Tree Table
- Exported Symbols
- Tips And Gotchas
- Architecture
- Build And Test
Requirements
- Angular
>=18 <22 @angular/core,@angular/common,@angular/forms
Install
npm install cats-data-gridIf you consume it from this workspace instead of npm, build it first:
ng build cats-data-gridConfigure Assets And Styles
The grid loads icons (images/*.svg) and SCSS from the package. Register both
in your application's angular.json.
{
"assets": [
{
"glob": "**/*",
"input": "node_modules/cats-data-grid/assets",
},
],
"styles": ["node_modules/cats-data-grid/styles/_index.scss"],
}Icons are referenced by relative paths such as
images/eye.svg. The assets entry above copies them so those paths resolve at runtime.
The components are standalone — import them directly where used:
import { Component } from "@angular/core";
import { CatsDataGridComponent, CommonRendererComponent } from "cats-data-grid";
@Component({
selector: "app-users",
standalone: true,
imports: [CatsDataGridComponent],
templateUrl: "./users.component.html",
})
export class UsersComponent {}Quick Start
Template
<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [totalRecords]="totalRecords" [paginationRequired]="true" [sortingRequired]="true" [filterRequired]="true" [checkBoxSelection]="true" [checkboxSelectionType]="'multiple'" [settingsRequired]="true" [threeDotsMenuRequired]="true" [pageSizeList]="[10, 20, 50]" [pageNumber]="pageNumber" [pageSize]="pageSize" [rowId]="'id'" (onPaginationChange)="onPaginationChange($event)" (onCheckboxSelection)="onCheckboxSelection($event)" (onRowClicked)="onRowClicked($event)" (onCellClicked)="onCellClicked($event)" (onCellEdit)="onCellEdit($event)" (onColConfigChange)="onColConfigChange($event)"></cats-data-grid>Component
import { Component } from "@angular/core";
import { CatsDataGridComponent, CommonRendererComponent } from "cats-data-grid";
@Component({
selector: "app-users",
standalone: true,
imports: [CatsDataGridComponent],
templateUrl: "./users.component.html",
})
export class UsersComponent {
pageNumber = 0;
pageSize = 20;
totalRecords = 3;
colDefs = [
{ headerName: "ID", fieldName: "id", width: 100, filterType: "number", headerLocked: true },
{ headerName: "Name", fieldName: "name", width: 200, filterType: "text", editable: true },
{
headerName: "Status",
fieldName: "status",
width: 160,
filterType: "set",
cellRenderer: CommonRendererComponent,
cellRendererParams: { type: "tag", tagKey: "name" },
},
{
headerName: "Action",
fieldName: "action",
width: 90,
filterable: false,
columnAction: false,
isAction: true,
cellRenderer: CommonRendererComponent,
cellRendererParams: {
type: "action-menu",
actions: [
{ label: "View", value: "view", image: "images/eye.svg" },
{ label: "Edit", value: "edit", image: "images/edit.svg" },
],
onAction: (event: any) => this.onAction(event),
},
},
];
rowData = [
{ id: 1, name: "Aarav", status: [{ name: "Active" }] },
{ id: 2, name: "Diya", status: [{ name: "Pending" }] },
{ id: 3, name: "Kabir", status: [{ name: "Inactive" }] },
];
onPaginationChange(e: { page: number; pageSize: number }) {
this.pageNumber = e.page;
this.pageSize = e.pageSize;
}
onCheckboxSelection(rows: any[]) {}
onRowClicked(e: any) {}
onCellClicked(e: any) {}
onCellEdit(e: any) {}
onColConfigChange(fields: string[]) {}
onAction(e: any) {}
}Integrated Charts
The chart packages are optional
cats-charts, echarts and ngx-echarts are declared as optional peer
dependencies. Installing or updating cats-data-grid never pulls them in, and
an application that does not use [enableCharts] builds and ships without them
— the charting code is loaded through a guarded dynamic import(), so nothing
references those packages until a chart is actually created.
Install them only when you turn charting on:
npm install echarts ngx-echarts cats-chartsIf charts are enabled without the packages present, the grid emits a chart
error event explaining which package to install rather than failing the build.
The generic grid renderer uses cats-charts first. Bar/column, line/area,
pie, doughnut, scatter/bubble and heatmap charts are rendered by the matching
standalone component from cats-charts. The lazy ECharts fallback is used only
for catalogue entries that cats-charts does not currently expose, such as
combination, polar, statistical, hierarchical, waterfall and funnel charts.
Enable charting and classify columns. Users can then drag a range, right-click, and select Chart followed by a chart family and type:
colDefs = [
{ headerName: "Department", fieldName: "department", chartDataType: "category" },
{ headerName: "Month", fieldName: "month", chartDataType: "time" },
{ headerName: "Revenue", fieldName: "revenue", chartDataType: "series" },
{ headerName: "Internal ID", fieldName: "id", chartDataType: "excluded" },
];<cats-data-grid #grid [rowData]="rowData" [colDefs]="colDefs" [enableCharts]="true" [chartThemes]="['default', 'dark', 'vintage']" (chartCreated)="onChartCreated($event)" (chartOptionsChanged)="onChartOptionsChanged($event)" (chartRangeSelectionChanged)="onChartRangeChanged($event)" (chartDestroyed)="onChartDestroyed($event)"></cats-data-grid>Each chart provides Chart, Set Up and Customize panels plus Advanced Settings,
link/unlink and image download actions. All chart families in
CHART_TYPE_DEFINITIONS are also available through createRangeChart().
The same right-click menu includes Export → CSV Export and
Export → Excel Export. Excel export downloads an Excel-readable .xls
workbook. Both formats export every row currently present in the grid and all
active data columns; the selected chart range does not limit an export. Object
and array values use cellRendererParams.tagKey when configured, so tag data
such as [{ label: "Active" }, { label: "On Call" }] is exported as
Active, On Call rather than [object Object].
const chart = this.grid.createRangeChart({
chartType: "columnLineCombo",
cellRange: {
rowStartIndex: 0,
rowEndIndex: 11,
columns: ["month", "revenue", "cost"],
},
seriesChartTypes: [
{ field: "revenue", chartType: "groupedColumn" },
{ field: "cost", chartType: "line", secondaryAxis: true },
],
});
this.grid.openChartToolPanel(chart!.chartId, "format");
this.grid.downloadChart({ chartId: chart!.chartId, fileFormat: "png" });Save and restore
getChartModels() returns versioned, JSON-safe models containing chart type,
data setup, formatting, interactivity, link state and popup geometry. The
application owns storage:
localStorage.setItem("charts", JSON.stringify(this.grid.getChartModels()));
const saved = JSON.parse(localStorage.getItem("charts") ?? "[]");
this.grid.restoreChartModels(saved);Legacy unversioned models are migrated automatically. Functions such as custom aggregators and menu actions are deliberately not serialized and must be provided again through the grid inputs when restoring.
Chart APIs
createRangeChart,createPivotChart,createCrossFilterChartupdateChart,destroyChart,getChartRefopenChartToolPanel,closeChartToolPaneldownloadChart,getChartImageDataURLgetChartModels,restoreChartModels
Charts are linked by default. Unlinking freezes a data snapshot and removes its active range; relinking resumes updates from the grid.
Column Definition (colDefs)
Each item in colDefs describes one column.
| Property | Type | Description |
| ---------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| headerName | string | Column title shown in the header. |
| fieldName | string | Field read from row data. Dot paths (user.name, interface.status) work. |
| width | number | Column width in pixels. |
| minWidth | number | Minimum column width in pixels. |
| maxWidth | number | Maximum column width in pixels. |
| filterType | 'text' \| 'number' \| 'date' \| 'set' | Filter UI to show for the column. |
| sortable | boolean (default true) | Enables sorting for this column. |
| secondaryField | string | Tie-breaker field. When primary values are equal, rows sort by this field. Dot paths work. See Sorting. |
| filterable | boolean (default true) | Enables filtering for this column. |
| active | boolean | false hides the column initially (still toggleable in settings). |
| headerLocked | boolean | Keeps the column always visible; it cannot be hidden from settings. |
| editable | boolean \| ((row) => boolean) | Allows inline editing on double click (needs [isRowsEditable]="true"). Pass a callback to decide per row. |
| editType | 'input' \| 'textarea' | Editor used when editing. Defaults to input; use textarea for long text. |
| wrapText | boolean | Wraps cell text instead of truncating with an ellipsis. |
| align | 'left' \| 'center' \| 'right' | Aligns this column alone, header included. Overrides the table's textAlign; unset follows the table. See Text Alignment. |
| dateFormat | string | Display format for date columns. |
| category | string | Groups columns into sections inside the settings panel. |
| disableGrouping | boolean | Prevents the column from being used as a group. |
| pin | 'left' \| 'right' | Pins the column to a side on first render. |
| leftPinned / rightPinned | boolean | Reflects/controls the current pinned side. |
| headerIcon | string | Icon path shown next to the header text. |
| tooltipText | string | Tooltip shown on the header. |
| addClass | (row) => string | Returns a CSS class applied to each cell in the column, per row. |
| cellRenderer | Component \| ((params) => string) | Angular component or function returning HTML for the cell. |
| cellRendererParams | object | Configuration passed to the renderer (see Renderers). |
| isAction | boolean | Marks a sticky action column (pinned right, not draggable). |
| columnAction | boolean | Shows/hides the per-column three-dot menu. |
colIdis assigned internally and does not need to be set.
Inline (function) cell renderer
A quick way to render custom HTML without a component. params.data is the row:
{
headerName: "Captured On",
fieldName: "capturedOnDate",
cellRenderer: (params: any) =>
`<span>${params.data.capturedOnDate}</span><br><small>${params.data.capturedOnTime}</small>`,
}Data Grid Inputs
| Input | Type | Default | Description |
| -------------------------- | ------------------------ | ------------------- | ------------------------------------------------------------------------- |
| rowData | any[] | [] | Rows to display. Required. |
| colDefs | any[] | [] | Column definitions. Required. |
| tableOptions | any | undefined | Extra options: noDataTemplate, addClasses(row). See below. |
| totalRecords | number | 0 | Total record count (drives pagination totals). |
| sortingRequired | boolean | true | Enables sorting. |
| filterRequired | boolean | true | Enables filtering. |
| paginationRequired | boolean | true | Shows the pagination bar. |
| checkBoxSelection | boolean | false | Shows row selection checkboxes. |
| checkboxSelectionType | 'single' \| 'multiple' | 'multiple' | Single (radio) or multiple (checkbox) row selection. |
| cellSelectionEnabled | boolean | true | Single-click whole-cell selection (blue outline). Set false to disable. |
| settingsRequired | boolean | true | Enables the column-settings panel. |
| settingsClicked | boolean | false | Open/close the settings panel from the parent. |
| threeDotsMenuRequired | boolean | true | Shows the per-column action menu. |
| groupByRequired | boolean | true | Enables the drag-to-group panel. |
| groupByField | string[] | [] | Initial group fields. |
| dynamicGroupingFiltering | boolean | false | Emit grouping/filter events instead of grouping locally (server-side). |
| appliedFilters | ColumnFilter[] | [] | Filters applied on first render. |
| pageSizeList | number[] | [20, 50, 75, 100] | Page-size dropdown options. |
| pageNumber | number | undefined | Current (zero-based) page number. |
| pageSize | number | undefined | Current page size. |
| resetPage | boolean | true | Reset to page 1 when total records change. |
| rowId | string | null | Unique row-id field. Recommended for stable selection/drag. |
| rowGripFieldName | string | undefined | Column whose cell shows a drag grip for row reordering. |
| bigRows | boolean | false | Taller rows. |
| showHeader | boolean | true | Renders the column header. false also disables row grouping. See Table Appearance. |
| borders | GridBorders | 'horizontal' | Which grid lines the table draws. See Table Appearance. |
| stripedRows | boolean | false | Shades alternate rows. See Table Appearance. |
| textAlign | GridTextAlign | (not set) | Aligns every column, header included. Unset keeps each renderer's own alignment. See Table Appearance. |
| height | number | 400 | Grid body height in pixels. |
| isRowsEditable | boolean | false | Master switch for inline cell editing. |
| isLoading | boolean | false | Infinite-scroll loading indicator. |
| hasMoreData | boolean | true | Whether infinite scroll should keep requesting. |
| isScrollPagination | boolean | false | Use infinite scroll instead of the pager. |
| showSkeleton | boolean | false | Show skeleton loading rows. |
| skeletonRowsLength | number | 8 | Skeleton row count. |
| skeletonColsLength | number | 8 | Skeleton column count. |
| selectedRowEmpty | boolean | false | Clears current row selection when set true. |
| showRowNumber | boolean | false | Shows the fixed 40px row-number column, pinned left of the checkbox. |
| rowNumberHeader | string | '#' | Header text of the row-number column. |
| rowNumberStart | number | 1 | Number given to the first rendered row. |
| masterDetail | boolean | false | Enables expandable detail rows (adds a 40px expander gutter). |
| detailField | string | 'detailData' | Row property holding the DetailField[] for that row. |
| detailLoadingText | string | 'Loading details...' | Shown while an expanded row waits for its details. |
| detailEmptyText | string | 'No details available' | Shown when a row has no details. |
| showFooter | boolean | false | Shows the pinned summary row under the table. |
| footerLabel | string | 'Total' | Label shown under the first column of the footer. |
| footerValues | Record<string, any> | {} | Footer aggregates, keyed by column fieldName. |
Data Grid Events
| Event | Payload | Description |
| --------------------- | ------------------------------------ | ----------------------------------------------------- |
| onPaginationChange | { page: number, pageSize: number } | Page or page size changed (page is zero-based). |
| onCheckboxSelection | any[] | Current selected rows. |
| onRowClicked | { row } | A row was clicked. |
| onCellClicked | { row, col } | A cell was single-clicked. |
| onCellEdit | { row, col, changedValue } | An editable cell value changed. |
| detailRowToggled | DetailRowToggleEvent | A master row expanded or collapsed (needsData asks the host to fetch). |
| onColConfigChange | string[] | Visible column field names (after show/hide/reorder). |
| appliedFiltersEvent | ColumnFilter[] | Current applied filters. |
| activeGroupsEvent | string[] | Active group fields. |
| onScrollEmitter | void | Table scrolled to the bottom (infinite scroll). |
| filter | any | Filter data changed. |
| onHideSettings | boolean | Settings panel was closed. |
tableOptions
tableOptions = {
noDataTemplate: this.noDataTemplateRef, // TemplateRef shown when there are no rows
addClasses: (row: any) => (row.isUrgent ? "urgent" : ""), // per-row CSS class
};Master / Detail Rows
Set masterDetail to give every row an expander in a fixed 40px gutter (between
the row numbers and the selection checkbox). Expanding a row reveals its details
as label/value pairs, each rendered in a real table cell under the column it
belongs to — so the panel lines up with the table, and wraps onto another line
once the fields outnumber the columns.
<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [masterDetail]="true" (detailRowToggled)="onDetailRowToggled($event)"></cats-data-grid>Details are a DetailField[] on the row:
interface DetailField {
label: string; // caption
value: any; // rendered underneath the caption
span?: number; // columns to cover (default 1)
}Case 1 — fetch on expand
detailRowToggled fires on every expand and collapse. When it reports
needsData: true the row has nothing to show yet: fetch it and assign the
result. Assign [] on failure so the panel stops showing its loading text.
onDetailRowToggled(event: DetailRowToggleEvent): void {
if (!event.needsData) return;
this.api.getShipmentDetails(event.rowId).subscribe({
next: (data) =>
this.grid.setDetailData(event.row, [
{ label: "Destination Country", value: data.country },
{ label: "Destination Region", value: data.region },
{ label: "Source Region", value: data.sourceRegion },
{ label: "Destination Port", value: data.port },
]),
error: () => this.grid.setDetailData(event.row, []),
});
}setDetailData(row, fields) is a convenience — assigning row.detailData = […]
does exactly the same thing.
Case 2 — prefetched
If the rows already carry their details, nothing else is needed: expanding
renders them immediately and needsData is false.
rowData = [
{
id: 1,
flowId: "FL-8841",
detailData: [
{ label: "Destination Country", value: "India" },
{ label: "Destination Region", value: "South Asia" },
{ label: "Source Region", value: "Southeast Asia" },
{ label: "Destination Port", value: "78912" },
],
},
];Behaviour notes
- The panel scrolls horizontally with the table; it does not track pinned columns. Every data column can hold a field, pinned ones included; only the action column is left empty.
- A value longer than its column wraps inside the cell (
overflow-wrap: anywhere), so even an unbroken run never widens the column or overflows. - Fields wrap onto a new line when they exceed the visible data columns. Action columns are left empty.
- Expansion survives a data refresh for rows that are still present; rows that disappear lose their state.
- Imperative control:
expandDetailRow(row),collapseDetailRow(row),collapseAllDetailRows(),isRowExpanded(row).
Built-In Cell Renderers
Import CommonRendererComponent and set it as cellRenderer. Behavior is chosen
by cellRendererParams.type: tag, link, switch, action-menu.
Tag
Renders one or more tags/badges from a string, an array of strings, or an array
of objects (read the display value with tagKey).
{
headerName: "Status",
fieldName: "status",
filterType: "set",
cellRenderer: CommonRendererComponent,
cellRendererParams: {
type: "tag",
subType: "badge", // optional: badge style
tagKey: "name", // key read from object values
maxVisible: 2, // tags shown inline before the rest collapse (default 2)
cellValueClass: (params: any) => params.value?.class, // optional per-tag class
},
}Tags beyond maxVisible collapse behind a +N that reveals the full list on
hover.
Styling tags
tagStyle is one optional block; omit it and tags render as they always have.
cellRendererParams: {
type: "tag",
tagKey: "name",
maxVisible: 2,
tagStyle: {
shape: "badge", // "tag" (default, a chip) | "badge" (a full pill)
variant: "filled", // "stroke" (default) | "filled"
big: true, // a taller pill
placement: "trailing", // which side the dot or image sits on
colors: ["#2680EA", "#733FB7"], // by pill *position*
colorField: "tone", // a colour on the datum; wins over position
tagClass: (tag, index, row) => (tag.tagValue === "Failed" ? "bg_error" : ""),
},
}A colour is either a bare string — filled fills it and letters in white, stroked
outlines it and letters in a darker shade — or a design system's own pairing:
{ strong, surface, border, text, textOnFill }.
There are three ways to colour a tag by its value, in increasing order of how much the host has to supply:
| | How | Needs |
| --- | --- | --- |
| class on the datum | [{ name: "Active", class: "bg_active" }] | CSS |
| tagStyle.tagClass | a callback per tag | CSS |
| tagStyle.colorField | a colour on the datum | nothing |
tagClass is the per-tag counterpart of a column's addClass (per row) and
cellValueClass (per cell): tags in the same cell can be classed differently.
It receives the transformed tag — tagValue plus every property of the datum it
came from — and runs once per tag when the value changes, not once per
change-detection pass. The renderer keeps up with live data — the grid refreshes it whenever the
row or value changes — and a tag value of 0 or false renders as itself rather
than as N/A.
Link
{
headerName: "Customer",
fieldName: "customer.name",
cellRenderer: CommonRendererComponent,
cellRendererParams: {
type: "link",
onLinkClick: (event: any) => this.onCustomerClick(event),
},
}Switch (toggle)
{
headerName: "Enabled",
fieldName: "enabled",
cellRenderer: CommonRendererComponent,
cellRendererParams: {
type: "switch",
onToggle: (event: any) => this.onStatusToggle(event), // { params, row, value }
},
}Action Menu
{
headerName: "Action",
fieldName: "action",
filterable: false,
columnAction: false,
isAction: true,
cellRenderer: CommonRendererComponent,
cellRendererParams: {
type: "action-menu",
subType: "vertical",
actions: [
{ label: "View", value: "view", image: "images/eye.svg" },
{ label: "Edit", value: "edit", icon: "edit", disabled: true },
{ label: "Delete", value: "delete", icon: "trash", class: "text-danger" },
],
onAction: (event: any) => this.onAction(event),
},
}Every action can also show, hide or disable itself per row — see Conditional action-menu items.
Identity Cell Renderers
Four standalone renderers for the common "who / what is this row" cell. Unlike
the CommonRendererComponent family above, each one is its own component passed
directly as cellRenderer — they are OnPush and compute their whole view once
per refresh, so a column of them stays cheap on a long page.
| Component | Cell |
| --- | --- |
| TextCellRendererComponent | Heading + subheading |
| InitialsCellRendererComponent | Initials disc + text |
| AvatarCellRendererComponent | Avatar image + text |
| IconCellRendererComponent | Icon + text |
The heading always comes from the column's fieldName. Only
TextCellRendererComponent renders a second line; the media renderers show a
single line with their avatar, icon or initials.
Heading + subheading
The subheading comes from cellRendererParams.secondaryField, or from the
column's own secondaryField when that is not set — the same property that acts
as the secondary sort key, so one declaration
covers both.
{
headerName: "Account",
fieldName: "accountName",
secondaryField: "accountTier", // subheading *and* sort tie-breaker
cellRenderer: TextCellRendererComponent,
cellRendererParams: {
alignment: "vertical", // "vertical" (default) | "horizontal"
separator: "·", // horizontal only
primaryStyle: { fontWeight: 600 },
secondaryStyle: { color: "var(--text-link-default)" },
},
}Initials, avatar and icon
// Initials derived from the cell value: "Jane Doe" -> JD, "Acme" -> A
{ headerName: "Owner", fieldName: "ownerName",
cellRenderer: InitialsCellRendererComponent }
// Avatar image, falling back to initials and then to a placeholder
{ headerName: "Assignee", fieldName: "assigneeName",
cellRenderer: AvatarCellRendererComponent,
cellRendererParams: {
imageField: "assigneeAvatar", // URL or base64 data URI on the row
placeholder: "images/user.svg",
} }
// Icon, falling back to a static icon and then to a placeholder
{ headerName: "Service", fieldName: "serviceName",
cellRenderer: IconCellRendererComponent,
cellRendererParams: {
iconField: "serviceIcon",
icon: "images/server.svg",
placeholder: "images/image.svg",
} }Avatar and icon take an alignment. "vertical" puts the media on top with
the value below it; "horizontal" — or leaving it unset — keeps the media
beside the value.
{ headerName: "Assignee", fieldName: "assigneeName",
cellRenderer: AvatarCellRendererComponent,
cellRendererParams: {
imageField: "assigneeAvatar",
alignment: "vertical", // avatar on top, value below
} }Fallback order, applied per row — an image that fails to load counts as absent:
- avatar —
imageField→ initials (unlessshowInitials: false) →placeholder - icon —
iconField→icon→placeholder(never initials) - initials — derived initials →
placeholder
Badge
Any of the four renderers accepts a badge block. Configuring a badge at all
wraps the primary line in a pill; style chooses what goes inside it, and
"none" — the default — is a bare pill.
cellRendererParams: {
badge: {
style: "dot", // "none" (pill only) | "dot" | "icon"
placement: "leading", // "leading" (default) | "trailing"
colorField: "statusColor", // per-row dot colour; or a fixed `color`
titleField: "statusLabel", // accessible label; or a fixed `title`
// pillStyle: { background: "#f4f5f6" }, // restyle the pill itself
},
}For style: "icon", supply icon (a path or base64 data URI) or iconField.
An icon that resolves to nothing leaves the pill in place without an adornment.
variant: "filled" fills the pill instead of outlining it, and big: true
gives it more padding.
Multiple tags in one cell
When the cell value is an array, the badge draws one pill per entry.
maxVisible caps how many are shown and the rest collapse behind a +N whose
tooltip names them; colors colours the pills by position, so the first pill
on every row shares a colour whatever it says:
cellRendererParams: {
badge: {
variant: "filled",
maxVisible: 2, // then "+3"
colors: ["#2680EA", "#29A277"], // pill 1, pill 2
showCount: true, // default
},
}A scalar value is unaffected — it stays the single pill the renderer has always drawn, so nothing changes for a column configured before this existed.
Only the first line is wrapped, so a heading + subheading column shows the pill around the heading with the subheading below it.
Renderer params
| Param | Type | Default | Applies to |
| --- | --- | --- | --- |
| secondaryField | string | column's secondaryField | text |
| alignment | 'vertical' \| 'horizontal' | 'vertical' | text; avatar, icon (media stacks above the value only when set to 'vertical') |
| separator | string | — | text, horizontal only |
| primaryStyle / secondaryStyle | style object | — | all |
| emptyText | string | 'N/A' | all |
| tooltip | boolean | true | all |
| imageField | string | — | avatar |
| iconField / icon | string | — | icon |
| placeholder | string | — | initials, avatar, icon |
| showInitials | boolean | true | avatar |
| initials | (row) => string | derived from the value | initials, avatar |
| mediaSize | number (px) | 28 | initials, avatar, icon |
| mediaShape | 'circle' \| 'square' \| 'rectangle' | 'circle', 'square' for icons | initials, avatar, icon |
| mediaWidth / mediaHeight | number (px) | mediaSize, width 42 for a rectangle | initials, avatar, icon |
| cornerRadius | number (px) | 4 | square and rectangular slots; a circle ignores it |
| mediaColor | string | — | initials — fills the disc, and the text colour is derived from it; icon — tints the glyph (see below) |
| truncate | boolean | true | all — false wraps the text instead of ellipsising it |
| badge | BadgeConfig | — | all |
Tinting an icon
mediaColor on IconCellRendererComponent recolours the glyph. An <img>
cannot be recoloured, so the icon is used as a mask and the colour painted
behind it — which means only the glyph's shape survives:
- A single-colour glyph, like the bundled icon set, tints exactly as expected.
- A multicolour or photographic source is flattened to the one colour.
- A cross-origin URL may fail to load as a mask where the same URL loads fine in
an
<img>. A data URI never does.
Leave mediaColor unset to draw the icon as-is.
Value placement
Renderers that pair a number with a visual take valuePlacement, which puts the
number on either side of it:
| Renderer | Default | 'lead' | 'tail' |
| --- | --- | --- | --- |
| RatingCellRendererComponent | 'tail' | value, stars | stars, value |
| MeterCellRendererComponent | 'tail' | value, bar | bar, value |
| SparklineCellRendererComponent | 'tail' | value, chart | chart, value |
| ValueDeltaChartCellRendererComponent | 'lead' | value, chart | chart, value |
The value+delta+chart renderer is the odd one out: it has always led with its
text, so 'tail' is the override there. Leave it unset and each renderer keeps
the order it has always drawn.
Writing your own
The contract is open — any component with a cellInit method can be a
cellRenderer, and adding cellUpdate opts it into live data:
export class MyCellRenderer {
cellInit(params: CellRendererParams, api?: any): void { /* … */ }
cellUpdate(params: CellRendererParams, api?: any): void { /* … */ }
}params is { data, value, cellParams, col }. Extend BaseCellRenderer to
inherit OnPush refresh handling plus rawValue(field?), which reads straight
off the row so 0, '' and false are not mistaken for missing values.
Numeric Cell Renderers
Five renderers for numeric columns. Like the identity renderers they are
standalone OnPush components passed straight to cellRenderer, and they read
values off the row directly — a genuine 0 renders as 0, never as N/A.
| Component | Cell |
| --- | --- |
| HeatmapCellRendererComponent | Cell filled with a colour scaled to the column |
| RatingCellRendererComponent | Star rating, halves included |
| DeltaCellRendererComponent | Change with a direction glyph |
| ValueDeltaCellRendererComponent | Value followed by its change |
| MeterCellRendererComponent | Linear meter, progress bar or banded gauge |
All five share the number-formatting options digits, minDigits, locale,
prefix, suffix, emptyText and format (a (value) => string override that
wins over the rest).
Heatmap
Paints the whole cell from a sequential ramp according to where the value sits
in its column. The scale defaults to the column's own minimum and maximum in the
rendered rows, so filtering rescopes the colours; pass min / max to pin a
fixed scale. Text flips to white over the dark end of the ramp automatically.
{
headerName: "Revenue", fieldName: "revenue",
cellRenderer: HeatmapCellRendererComponent,
cellRendererParams: {
palette: "amber", // "blue" | "amber" | "green" | "red" | "grey", or a colour array
// min: 0, max: 100000, // pin the scale instead of deriving it
// reverse: true, // high values take the light end
// align: "right", // "left" | "center" | "right" (default "right")
},
}Rating
Each star occupies a 24x24 box with a 20x20 glyph centred in it, so the boxes
sit flush while the stars stay visually separated. Resizing size keeps that 2px
inset; set starSize to control the glyph directly.
A partial star fills in proportion to the value — 4.3 leaves the last star
three-tenths full. Set roundToHalf: true to snap partials to halves instead, or
allowHalf: false for whole stars only.
{
headerName: "Rating", fieldName: "rating",
cellRenderer: RatingCellRendererComponent,
cellRendererParams: {
max: 5, // default 5
allowHalf: true, // default true; partial stars fill proportionally
roundToHalf: false, // default false; true snaps partials to halves
showValue: true, // default true
valueFormat: "value-of-max", // renders "4.5/5" instead of "4.5"
size: 24, // the box one star occupies, in px
starSize: 20, // the star itself; defaults to `size` - 4
},
}Delta and value + delta
The change comes off the row — deltaField for the absolute figure and
deltaPercentField for the percentage. mode picks what is shown:
'absolute' (the default), 'percentage', or 'both', which reads as
▲ 16.3% (120).
behavior: 'inverse' is for columns where lower is better — latency, error
rate, cost. The glyph still points the way the value actually moved; only the
colour flips, so the cell never lies about the direction.
// Change on its own — the column's own field is the change
{ headerName: "Change", fieldName: "revenueDelta",
cellRenderer: DeltaCellRendererComponent,
cellRendererParams: {
deltaPercentField: "revenueDeltaPct",
mode: "both",
directionIndicator: true, // default true; the ▲ / ▼ glyph
behavior: "default", // "inverse" when lower is better
percentDigits: 1,
} }
// Value + change — the column's field is the value, so name the delta fields
{ headerName: "Revenue", fieldName: "revenue",
cellRenderer: ValueDeltaCellRendererComponent,
cellRendererParams: {
deltaField: "revenueDelta",
deltaPercentField: "revenueDeltaPct",
mode: "both",
valueDigits: 0,
} }When directionIndicator is on the number is shown unsigned, because the glyph
already carries the sign; turn it off and the sign comes back.
Linear meter
Values are percentages unless maxValue is given, in which case they are
normalized against it. Two thresholds split the track: below lowerBand is red,
between the bands amber, at or above upperBand green. The target is always
100% (the end of the scale, i.e. maxValue when given), which showTarget marks.
{
headerName: "Utilisation", fieldName: "utilisation",
cellRenderer: MeterCellRendererComponent,
cellRendererParams: {
lowerBand: 60, // default 60
upperBand: 90, // default 90
fillType: "segmented", // "continuous" (default) | "segmented"
segments: 8, // default 5
colorByBands: true, // colour each block by the band it sits in
showTarget: true, // default false
showValue: true, // default true
// maxValue: 250, // normalize a raw value into a percentage
},
}colorByBands colours each block by its position on the track rather than by
the value's band, so the bands stay readable whatever the value — with eight
segments and the defaults that is four red, three amber, one green.
For a plain progress bar, set a fixed color to bypass the bands entirely:
cellRendererParams: { color: "#0d6efd", showValue: false }Column ranges
ColumnStatsService computes the per-column numeric range the heatmap scales
against. It is provided per grid instance and memoised on the identity of the
rendered rows, so the scan is shared across every cell in the column rather than
repeated per cell. Renderers outside a grid that provides it fall back to the
explicit min / max params. After mutating rows in place, call
invalidate() on it to force a rescan.
Chart Cell Renderers
Two renderers draw a chart inside the cell, using cats-charts — the same
optional peer dependency as Integrated Charts. It is
loaded through a single shared, lazy import(), so a page full of sparklines
still fetches the package once, and a cell degrades to a dash rather than
failing when the package is not installed.
| Component | Cell |
| --- | --- |
| SparklineCellRendererComponent | Line, area or column chart |
| ValueDeltaChartCellRendererComponent | Value, change and chart together |
Sparkline
The series is an array of numbers on the row. By default the plot is coloured by
direction — where the series ended against where it started — with
behavior: 'inverse' flipping which of those reads as good, exactly as on the
delta renderers.
{
headerName: "Revenue trend", fieldName: "revenueSeries",
sortable: false, filterable: false,
cellRenderer: SparklineCellRendererComponent,
cellRendererParams: {
variant: "area", // "line" (default) | "area" | "column"
// field: "otherField", // read the series from a different field
// pointField: "value", // when the array holds objects
colorMode: "direction", // "direction" (default) | "fixed"
// color: "#0d6efd", // fixed mode, and the flat case
height: 26, // default 26
showTooltip: false, // off by default — noisy at one chart per row
// barWidth: "60%", // column variant
},
}Set showValue to draw the series' own number beside the chart. A chart
column's cell value is the series, so the number has to be named separately
through valueField — without one, nothing is drawn:
cellRendererParams: {
variant: "area",
showValue: true,
valueField: "revenueTotal", // a scalar kept alongside the series
valuePlacement: "tail", // "lead" puts it before the chart
valueStyle: { fontWeight: 600 },
}The column variant keeps a true zero baseline, so negative points hang below it instead of being drawn upward — pass a series that crosses zero to see it.
Value + delta + chart
Everything ValueDeltaCellRendererComponent accepts, plus the sparkline options.
field names the series; the column's own field stays the value. The chart takes
the delta's tone colour, so the number and the trend always agree.
{
headerName: "Revenue trend", fieldName: "revenue",
sortable: false, filterable: false,
cellRenderer: ValueDeltaChartCellRendererComponent,
cellRendererParams: {
field: "revenueSeries",
deltaField: "revenueDelta",
deltaPercentField: "revenueDeltaPct",
mode: "absolute",
variant: "line",
},
}Progress bar
A plain progress bar is MeterCellRendererComponent with a fixed colour, which
bypasses the threshold bands — see Linear meter:
cellRendererParams: { color: "#0d6efd", showValue: false }Cost
Each chart cell mounts a real charting instance, so a chart column is
meaningfully heavier than the SVG-free renderers above. Keep such columns to the
ones that earn their place, prefer paging over very long scrolls, and leave
showTooltip off unless a column really needs it. Sorting and filtering on a
series column rarely make sense either — the examples switch both off.
Sorting And Secondary Sort
Sorting is on by default ([sortingRequired]="true"). Click a header (or use the
three-dot menu) to cycle ascending → descending → none. Strings sort with
locale-aware comparison; numbers and dates compare naturally.
Add secondaryField to break ties: when two rows have equal primary values,
they are ordered by the secondary field (same direction as the primary sort). Dot
paths are supported for both fields.
colDefs = [
// Sort by status; rows with the same status are ordered by capturedOn.
{ headerName: "Status", fieldName: "status", secondaryField: "capturedOn" },
// Works with nested fields too.
{ headerName: "Link Status", fieldName: "interface.status", secondaryField: "interface.name" },
];Filtering
Set filterType per column to choose the filter UI and operators.
| filterType | UI | Operators |
| ------------ | --------------- | -------------------------------------------------------------------------- |
| text | text box | Contains, Does Not Contain, Equals, Does Not Equal, Starts With, Ends With |
| number | number box | =, !=, >, <, >=, <= |
| date | calendar picker | date match |
| set | checkbox list | select one or more values (options are derived from the data) |
Text/number filters support two conditions combined with AND / OR
(filterLogic). Pre-apply filters with appliedFilters:
appliedFilters: ColumnFilter[] = [
{
fieldName: "readingId",
filterLogic: "OR",
filters: [
{ filterOperation: "contains", filterValue: "002" },
{ filterOperation: "startsWith", filterValue: "RD" },
],
},
];Listen with (appliedFiltersEvent)="...". For server-side filtering, see
Server-Side Data.
Set filter options
A set filter's options are the distinct values found in the column's data. They
are seeded when colDefs arrives and then re-derived whenever rowData
changes, so a grid fed a fresh page from a paginated API offers the values of
the data it currently holds — the host never has to reassign colDefs to
refresh them.
The tick state is reconciled with what still exists:
| Column state | On new data | | --- | --- | | Everything ticked (not filtering) | Adopts the new options, all ticked — new values stay visible | | Narrowed to a subset | Keeps your choices; new values are listed unticked so the filter is not widened behind you | | A chosen value disappears | Dropped from the selection | | Every chosen value disappears | The filter clears, rather than matching nothing with no value left to untick |
Any text in the panel's search box is re-applied to the new list.
Options are read from the unfiltered rows, so an active filter on one column does not shrink its own option list.
<cats-data-grid [refreshFilterOptionsOnDataChange]="false"></cats-data-grid>Turn it off when the host supplies its own option lists, or when the grid only ever receives a slice of the data and the options must describe the whole set rather than the slice.
Pagination And Infinite Scroll
Pager (default):
<cats-data-grid [paginationRequired]="true" [totalRecords]="totalRecords" [pageSizeList]="[10, 20, 50]" [pageNumber]="pageNumber" [pageSize]="pageSize" (onPaginationChange)="onPaginationChange($event)"></cats-data-grid>onPaginationChange emits { page, pageSize } with a zero-based page.
Client-side pagination
By default the grid is a window onto data the host pages server-side: it renders
whatever rowData it is given and emits onPaginationChange so the host can
fetch the next slice.
Set clientSidePagination and the grid holds the whole set, slices it itself,
counts the pages from the rows it has, and emits nothing:
<cats-data-grid [rowData]="allRows" [paginationRequired]="true" [clientSidePagination]="true" [pageSize]="50"></cats-data-grid>totalRecordsis ignored — the count comes from the rows, so it stays correct after filtering.- Sorting and filtering still run across every row, not just the visible page.
- Filtering that shortens the data clamps the current page back into range, rather than leaving an empty grid on a page that no longer exists.
- When the grid is also grouped, the page is taken first and the grouping describes that page.
onPaginationChangedoes not fire. Nothing for the host to handle.
Default page size. pageSize sets the rows per page, and the page-size
dropdown always offers it — if it is not one of the pageSizeList entries the
grid adds it, in sorted position when the list is ascending and appended
otherwise. So this is valid, and the dropdown shows 7, 10, 20, 50:
<cats-data-grid [pageSizeList]="[10, 20, 50]" [pageSize]="7"></cats-data-grid>The same applies to <cats-tree-table>, which also accepts [pageSize].
Infinite scroll — swap the pager for scroll loading:
<cats-data-grid [isScrollPagination]="true" [isLoading]="isLoading" [hasMoreData]="hasMoreData" (onScrollEmitter)="loadMore()"></cats-data-grid>Row Selection
<cats-data-grid [checkBoxSelection]="true" [checkboxSelectionType]="'multiple'" [rowId]="'id'" (onCheckboxSelection)="onCheckboxSelection($event)"></cats-data-grid>'multiple'— checkboxes with a select-all header checkbox.'single'— radio buttons.- Provide
rowIdso selection survives re-renders. - A row with
isLocked: trueis selected and cannot be unchecked;isSelected: truepre-selects a row. - Set
[selectedRowEmpty]="true"to clear the current selection programmatically.
Cell Selection
Single-clicking a cell outlines the whole cell with a focus border. It is on by default and independent of editing.
<!-- default: enabled -->
<cats-data-grid ...></cats-data-grid>
<!-- disable -->
<cats-data-grid [cellSelectionEnabled]="false" ...></cats-data-grid>(onCellClicked) emits { row, col } for the clicked cell.
Inline Cell Editing (Input / Textarea)
Turn on the master switch and mark editable columns. Double-click a cell to edit;
the editor fills the column width and its text is auto-selected. input saves on
Enter or blur; textarea saves on blur (Enter inserts a newline).
<cats-data-grid [isRowsEditable]="true" (onCellEdit)="onCellEdit($event)"></cats-data-grid>colDefs = [
{ headerName: "Reading ID", fieldName: "readingId", editable: true }, // single-line input
{ headerName: "Notes", fieldName: "notes", editable: true, editType: "textarea", wrapText: true }, // multi-line
];
onCellEdit(e: { row: any; col: any; changedValue: any }) {
// persist e.changedValue for e.row / e.col
}editType defaults to 'input'. Dot-path fields (e.g. hcpNotes.name) are
supported for editing.
Conditional (per-row) editing — pass a callback to editable instead of a
boolean. It receives the row and returns whether that specific cell can be edited:
colDefs = [
{
headerName: "Notes",
fieldName: "notes",
editType: "textarea",
// Only rows that are not locked can be edited.
editable: (row: any) => !row.isLocked,
},
];Non-editable cells simply won't enter edit mode on double click (and don't get the editable-field styling).
Row Grouping
Enable the drag-to-group panel and (optionally) seed initial groups:
<cats-data-grid [groupByRequired]="true" [groupByField]="['status']" (activeGroupsEvent)="onGroups($event)"></cats-data-grid>- Drag a column into the group panel, or use the column's three-dot Group by action.
- Multiple levels of grouping are supported (nested group rows).
- Columns with
disableGrouping: truecannot be grouped. - Group rows render with a chevron and a
(count); click to expand/collapse. - For server-driven grouping, set
[dynamicGroupingFiltering]="true"and handleactiveGroupsEvent/appliedFiltersEvent.
Column Pinning
Pin a column on first render with pin, or let users pin via the three-dot menu.
colDefs = [
{ headerName: "ID", fieldName: "id", pin: "left" },
{ headerName: "Action", fieldName: "action", isAction: true }, // sticky right
];Column Settings Panel
With [settingsRequired]="true", users can show/hide and reorder columns.
- Group columns into sections using
category. - Keep a column permanently visible with
headerLocked: true. - Hide a column initially with
active: false. - Open/close the panel from the parent with
[settingsClicked], and react to changes via(onColConfigChange)(emits visible field names) and(onHideSettings).
Row Numbers
Set showRowNumber to render a fixed 40px counter column. It is always the
first cell in the row — ahead of the selection checkbox — and stays pinned while
the table scrolls horizontally. Pinned columns automatically shift right to
clear it, so no other configuration changes.
<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [showRowNumber]="true" [rowNumberHeader]="'#'" [checkBoxSelection]="true"></cats-data-grid>Numbering starts at rowNumberStart (default 1). With server-side paging the
grid only receives the current page, so pass the offset to keep numbering
continuous:
<cats-data-grid [showRowNumber]="true" [rowNumberStart]="pageNumber * pageSize + 1"> </cats-data-grid>The sticky gutters stack in this order, and left-pinned columns start after them:
| Gutter | Width | Sticky at |
| --- | --- | --- |
| Row number (showRowNumber) | 40px | left: 0 |
| Detail expander (masterDetail) | 40px | left: 40px |
| Selection (checkBoxSelection) | 50px | left: 80px |
| First left-pinned column | its own | left: 130px |
(Each gutter only occupies space when it is enabled; the offsets shift accordingly.)
Footer Totals
Set showFooter to pin a summary row under the table. It sticks to the bottom
of the scroll viewport, so the totals stay visible while rows scroll beneath
them, and pinned columns stay pinned inside it.
footerLabel is rendered under the first visible column; every other column
shows footerValues[fieldName]. The grid does not aggregate anything itself —
the host owns that, so it can sum, average, count or format however it needs.
// Component
get footerValues(): Record<string, any> {
const sum = (field: string) =>
this.rowData.reduce((total, row) => total + (Number(row[field]) || 0), 0);
return {
name: `${this.rowData.length} people`,
salary: sum('salary').toLocaleString('en-IN'),
bonus: sum('bonus').toLocaleString('en-IN'),
performance: `avg ${Math.round(sum('performance') / this.rowData.length)}`,
};
}<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [showFooter]="true" [footerLabel]="'Total'" [footerValues]="footerValues"></cats-data-grid>A column with no entry in footerValues renders an empty footer cell. If the
first column is hidden from the settings panel, the label moves to whichever
column is first at that moment.
Table Appearance
Table-level styling is set through inputs on the grid. Every one of them defaults to the appearance the grid has always had, so an existing grid that sets none of them is unchanged.
Hiding The Header
[showHeader]="false" leaves the body alone on screen:
<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [showHeader]="false"></cats-data-grid>The header carries most of the grid's per-column controls, so hiding it takes them with it — sorting, the filter popups, the per-column menu, column drag-to-reorder and the resize handles are all unavailable. Column widths are not affected: they come from the column definitions, not from the header row.
Row grouping goes too, because it is driven entirely from the header:
- the group panel is hidden,
groupByFieldis ignored,- the chart context menu drops its Group by … item (with
[enableCharts]that menu is the one grouping entry point outside the header), - and a grid that is already grouped when the header is hidden is ungrouped.
That last point matters: grouping a column from the header removes it from the table, so ungrouping is what puts it back. Hiding the header therefore restores any column that was grouped, rather than leaving it missing.
Columns hidden through the settings panel are unaffected — use
[settingsClicked] to drive that panel from the parent while the header is
hidden.
Zebra Rows
[stripedRows]="true" shades alternate rows:
<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [stripedRows]="true"></cats-data-grid>The stripe follows the data rows, not the rendered <tr> elements. Expanded
detail panels and group-header rows are rows too, and counting them would knock
the alternation out of step. Under grouping each group stripes from its own
first row.
Frozen cells stripe with their row — pinned and action columns, and the row-number, expander and selection gutters. Those cells need an opaque background so the scrolling columns cannot show through them, and they take it from the row rather than painting a fixed white.
Override --cats-row-stripe to retune the shade:
cats-data-grid { --cats-row-stripe: #fafbfc; }Text Alignment
[textAlign] aligns every column, header included:
<cats-data-grid [rowData]="rowData" [colDefs]="colDefs" [textAlign]="'center'"></cats-data-grid>| Value | Effect |
| ----------- | ----------------------------------------------------------------- |
| (not set) | The default. Cells read from the left and each renderer keeps its own alignment. |
| 'left' | Forces left, overriding renderer alignment. |
| 'center' | Centres every column. |
| 'right' | Right-aligns every column. |
Note that leaving it unset is not the same as 'left'. Unset changes nothing,
so a renderer that aligns itself — the heatmap right-aligns its numbers — goes
on doing so. Setting it to 'left' makes the table's choice win over that.
Two things deliberately stay put:
- The fixed gutters — row numbers, the detail expander, the selection checkbox — hold controls rather than data, so they keep their own alignment.
- The header's filter and menu icons stay on the right; only the column label moves.
A renderer that fills the width of its cell has nothing to align, so the meter bar and in-cell charts are unaffected.
Aligning one column
A column can opt out of the table's alignment with its own align, which wins
for that column alone — header included — and leaves every other column
following the table:
colDefs = [
{ headerName: "Account", fieldName: "accountName" }, // follows the table
{ headerName: "Revenue", fieldName: "revenue", align: "right" }, // its own
];Leave it unset to follow the table. It republishes the same custom properties below on that column's cells, so a renderer needs no sp
