{"id":50971889,"url":"https://github.com/thkl/agrid","last_synced_at":"2026-06-19T03:01:24.535Z","repository":{"id":362950916,"uuid":"1261367766","full_name":"thkl/agrid","owner":"thkl","description":"A Grid Component for angular","archived":false,"fork":false,"pushed_at":"2026-06-14T16:49:24.000Z","size":869,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-14T18:22:37.220Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thkl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-06T15:38:14.000Z","updated_at":"2026-06-14T16:49:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thkl/agrid","commit_stats":null,"previous_names":["thkl/agrid"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/thkl/agrid","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thkl%2Fagrid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thkl%2Fagrid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thkl%2Fagrid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thkl%2Fagrid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thkl","download_url":"https://codeload.github.com/thkl/agrid/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thkl%2Fagrid/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34515405,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-19T03:01:23.579Z","updated_at":"2026-06-19T03:01:24.506Z","avatar_url":"https://github.com/thkl.png","language":"TypeScript","funding_links":[],"categories":["Third Party Components"],"sub_categories":["Data Grids"],"readme":"# agrid\n\n`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, and custom cell renderers.\n\n\n[![npm version](https://img.shields.io/npm/v/@thkl/agrid.svg)](https://www.npmjs.com/package/@thkl/agrid)\n\n\n## Live Demo\n\n[https://thkl.github.io/agrid/](https://thkl.github.io/agrid/)\n\n## Quick Start\n\n```bash\nnpm install @thkl/agrid @angular/cdk\n```\n\n```ts\nimport { Component } from '@angular/core';\nimport { AgridComponent, AgridControl, AgridDataSource, AgridProvider, ColDef, GridEditEvent } from '@thkl/agrid';\n\nconst columns: ColDef[] = [\n  { field: 'id', header: 'ID', width: 70, editable: false, pinned: 'left' },\n  { field: 'name', header: 'Name', width: 160, filterable: true },\n  { field: 'hiredAt', header: 'Hire Date', width: 130 }, // auto-formatted as a date\n  { field: 'departmentId', header: 'Department', width: 140, filterable: true, groupable: true,\n    values: [\n      { value: 1, label: 'Engineering' },\n      { value: 2, label: 'Sales' },\n    ],\n  },\n];\n\n@Component({\n  selector: 'app-page',\n  imports: [AgridComponent],\n  template: `\n    \u003cagrid [provider]=\"gridProvider\" (cellEdit)=\"onCellEdit($event)\" /\u003e\n  `,\n})\nexport class PageComponent {\n  readonly columns = columns;\n  readonly ds = new AgridDataSource([\n    { id: 1, name: 'Alice', hiredAt: '2021-03-15', departmentId: 1 },\n    { id: 2, name: 'Bob',   hiredAt: '2022-07-01', departmentId: 2 },\n  ]);\n  readonly gridControl = new AgridControl({ allowRowReorder: true });\n  readonly gridProvider = new AgridProvider({\n    locale: 'en-US',\n    columns: this.columns,\n    datasource: this.ds,\n    control: this.gridControl,\n    showControlColumn: true,\n    showSidebar: true,\n    zebraStripes: true,\n    rowSelection: 'multi',\n  });\n\n  onCellEdit(event: GridEditEvent): void {\n    console.log(event);\n  }\n}\n```\n\n## Features\n\n- Angular 21 standalone component.\n- CDK virtual scrolling for large row sets.\n- Signal-based data source and control state.\n- Editable text cells and select editors for fixed value columns.\n- Keyboard navigation with auto-scroll to the active cell.\n- Type-to-edit, Enter/F2 edit, Tab/Enter commit, Escape cancel.\n- Undo/redo for edits, paste, and fill operations.\n- Cell range selection with Shift+arrow and Shift+click.\n- Clipboard copy/paste using TSV/CSV-like plain text.\n- Fill handle for repeating selected cell/range values down or right.\n- Find panel with Ctrl/Cmd+F, full filtered-dataset matching, and next/previous navigation.\n- Text filters, string/number/date condition filters, value filters, and single-column sorting.\n- Column menu with sort, clear sort, autosize, pin/unpin, hide, group, and clear filter actions.\n- Column resizing by drag and autosize by double-click.\n- Column reordering by header drag.\n- Split-pane pinned columns on the left.\n- Optional control column for row context actions and row reordering.\n- Row selection: none, single, or multi.\n- Grouping with expand/collapse and custom group actions.\n- Sidebar column visibility picker.\n- Add-row placeholder and automatic row insertion.\n- CSV export of visible filtered data rows.\n- **Date auto-formatting** — ISO strings and `Date` objects are detected and displayed as locale-formatted dates automatically.\n- **Zebra stripes** — alternating row shading for easier reading.\n- **Readonly mode** — disable all editing with a single input.\n- **Pagination** — built-in page controls driven by `AgridControl`.\n- **Custom cell renderers** — return HTML strings per column for rich cell content.\n- **Column autosize all** — fit every visible column to its content in one call.\n- **Master/detail rows** — expand any row to reveal a custom HTML detail panel beneath it.\n- **Pinned rows** — keep summary/total rows fixed at the top or bottom of the body.\n- **Row CSS classes** — apply conditional classes to whole rows via `getRowClass`.\n\n## Component API\n\n```html\n\u003cagrid [provider]=\"gridProvider\" (cellEdit)=\"onEdit($event)\" /\u003e\n```\n\n`AgridComponent` has a single input: `provider`. All grid options, data, and control state are supplied through `AgridProvider`. See [AgridProvider Configuration](#agridprovider-configuration) for the full option list.\n\n### Inputs\n\n| Input | Type | Default | Description |\n| --- | --- | --- | --- |\n| `provider` | `AgridProvider` | New empty provider | Supplies column definitions, data source, control state, and all grid options. |\n\n## Localization\n\nSet `locale` on `AgridProvider` to control built-in grid text and date formatting. Built-in text supports English (`en-*`) and German (`de-*`).\n\nThe default is `'auto'`, which reads `navigator.language` from the browser and falls back to `'en-US'` if the browser language is not supported.\n\n```ts\n// Auto-detect browser language (default — no need to set locale explicitly)\nreadonly gridProvider = new AgridProvider({ ... });\n\n// Pin to a specific locale\nreadonly gridProvider = new AgridProvider({ locale: 'de-DE', ... });\n```\n\n### Adding custom locale text\n\nUse `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'`).\n\n```ts\nreadonly gridProvider = new AgridProvider({ ... })\n  .addLocalization('fr-FR', {\n    addRow: 'Ajouter une ligne',\n    noRows: 'Aucune donnée',\n    rows: count =\u003e `${count} enregistrement${count === 1 ? '' : 's'}`,\n    groupBy: header =\u003e `Grouper par ${header}`,\n  })\n  .addLocalization('nl-NL', {\n    addRow: 'Rij toevoegen',\n    noRows: 'Geen rijen',\n  });\n```\n\n`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.\n\nThe `AgridLocaleTextOverrides` type covers all overridable labels.\n\n### Outputs\n\n| Output | Type | Description |\n| --- | --- | --- |\n| `cellEdit` | `GridEditEvent` | Emitted after a committed cell edit, paste, fill, undo, or redo changes a cell. |\n| `recordEdit` | `RecordEditEvent` | Emitted on the next microtask after an edit updates a row. Includes the row `index`, current `data`, exact `provider`, and `datasource`. |\n| `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. |\n| `rowRemoved` | `RecordEditEvent` | Emitted after deleting a row. Includes its former `index`, captured `data`, exact `provider`, and `datasource`. |\n| `prepareAddRecord` | `NewRecord` | Emitted after the grid inserts a blank row. Patch `event.datasource` to target the correct grid when multiple providers are rendered. |\n| `rowReorder` | `RowReorderEvent` | Emitted after the user drops a reordered row. The host must call `dataSource.moveRow()`. |\n| `rowSelect` | `RowSelectEvent \\| null` | Emitted when row selection changes. `null` means selection was cleared. |\n| `menuBarAction` | `string` | Emitted for every enabled menu-bar button or dropdown item with its configured `id`. |\n| `treeNodeClick` | `TreeNodeClickEvent` | Emitted when a generated path-tree branch node is clicked. |\n| `treeNodeDoubleClicked` | `TreeNodeClickEvent` | Emitted when a generated path-tree branch node is double-clicked. |\n| `cellInfo` | `CellInfoEvent\u003cT\u003e` | Emitted when a column's optional cell info icon is clicked. |\n| `filterChange` | `FilterChangeEvent` | Emitted for text filter changes when `serverSideFiltering` is enabled. |\n| `sortChange` | `SortChangeEvent` | Emitted for sort changes when `serverSideFiltering` is enabled. |\n\nUse `rowChanged` instead of `cellEdit` when an API should receive the complete row only after the\nuser finishes editing it:\n\n```html\n\u003cagrid [provider]=\"provider\" (rowChanged)=\"saveRow($event)\" /\u003e\n```\n\n```ts\nsaveRow(event: RowUpdateEvent\u003cPersonRow\u003e): void {\n  this.http.patch(`/api/people/${event.row.id}`, event.row).subscribe(() =\u003e {\n    this.grid()?.clearChangedCells(event.originalIndex);\n  });\n}\n```\n\nDuring inline editing, moving between fields in the same row does not emit `rowChanged`. The event\nfires when navigation enters another row, filter focus clears the active cell, or focus leaves the\ngrid. `recordEdit` and `cellEdit` continue to fire for each committed field mutation.\n\nEnable changed-cell markers when the user should see which values are waiting to be persisted:\n\n```ts\nreadonly provider = new AgridProvider\u003cPersonRow\u003e({\n  columns,\n  datasource,\n  showChangedCellIndicator: true,\n});\n```\n\nAfter a successful API request, call `clearChangedCells(index)` for the complete row,\n`clearChangedCells(index, ['name', 'email'])` for selected fields, or\n`clearChangedCells()` for every marker.\n\n## AgridProvider Configuration\n\nAll grid options are passed to `AgridProvider` at construction time:\n\n```ts\nreadonly gridProvider = new AgridProvider({\n  columns: this.columns,\n  datasource: this.ds,\n  control: this.gridControl,\n  zebraStripes: true,\n  showSidebar: true,\n  showControlColumn: true,\n  rowSelection: 'multi',\n  allowAddRows: true,\n  enableRowMarking: true,\n  confirmRowDelete: true,\n  readonly: false,\n});\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `columns` | `ColDef[]` | `[]` | Column definitions. |\n| `headerGroups` | `HeaderGroup[]` | `[]` | Labels for optional grouped column headers. |\n| `datasource` | `AgridDataSource` | New empty datasource | Row data container. |\n| `control` | `AgridControl` | New default control | Manages filters, sort, grouping, pagination, and undo/redo. |\n| `locale` | `string` | `'auto'` | BCP-47 locale tag for grid text and date formatting. `'auto'` reads `navigator.language` and falls back to `'en-US'`. |\n| `localization` | `AgridLocaleTextOverrides` | `undefined` | Overrides individual labels. See [Localization](#localization). |\n| `rowHeight` | `number` | `32` | Fixed row height in pixels. Required by CDK virtual scroll. |\n| `minHeight` | `string` | `undefined` | CSS min-height for the virtual body. Example: `'200px'`. |\n| `maxHeight` | `string` | `undefined` | CSS max-height for the virtual body. Example: `'500px'`. |\n| `allowAddRows` | `boolean` | `false` | Shows a `+ Add row` placeholder at the bottom when `autoAddRows` is `false`. |\n| `autoAddRows` | `boolean` | `false` | Automatically inserts a blank row when navigation moves past the last real row. |\n| `showControlColumn` | `boolean` | `false` | Shows a 24 px control column for row context actions and drag handles. |\n| `enableRowMarking` | `boolean` | `false` | Shows checkboxes in a 48 px control column and includes marked rows in every copy operation. |\n| `showSidebar` | `boolean` | `false` | Shows a collapsible column visibility sidebar. Requires `control`. |\n| `autoOpenDetail` | `boolean` | `false` | Opens the detail row automatically when a row is selected. |\n| `serverSideFiltering` | `boolean` | `false` | Emits filter/sort events instead of applying them locally and hides the value checklist. |\n| `filterDebounceMs` | `number` | `300` | Debounce delay for server-side `filterChange` events. Set to `0` to disable. |\n| `menuBarItems` | `AgridMenuBarItem\u003cT\u003e[]` | `[]` | Optional buttons above the headers. Buttons may expose additional dropdown commands. |\n| `sortOption` | `'single' \\| 'multi' \\| 'none'` | `'multi'` | Allows one sort, multiple sorts, or disables sorting. |\n| `rowSelection` | `'single' \\| 'multi' \\| 'none'` | `'none'` | Row selection behavior. |\n| `enterEditAction` | `'nothing' \\| 'nextColumn' \\| 'nextRow'` | `'nextRow'` | Behavior after pressing Enter while editing a cell. |\n| `groupDescription` | `((label: string) =\u003e string) \\| null` | `null` | Optional description text shown next to each group label. |\n| `groupActions` | `GroupAction[]` | `[]` | Actions shown in each group header menu. |\n| `cellMenuItems` | `(CellContextMenuItem \\| null)[]` | `[]` | Additional items in the cell right-click context menu. `null` inserts a divider. |\n| `zebraStripes` | `boolean` | `false` | Shades every other row. Override `--agrid-color-bg-stripe` to change the shade. |\n| `showChangedCellIndicator` | `boolean` | `false` | Marks committed cell changes until `clearChangedCells()` is called. |\n| `confirmRowDelete` | `boolean` | `false` | Fades the target row and shows a localized in-row Yes/No confirmation. |\n| `emptyText` | `string` | `undefined` | Text shown when the grid has no rows. Falls back to the locale default. |\n| `readonly` | `boolean` | `false` | Initial value for the readonly signal. Makes all cells non-editable. |\n| `loading` | `boolean` | `false` | Initial value for the loading signal. Shows a loading overlay over the grid body. |\n| `getRowClass` | `(p: { row; index }) =\u003e string` | `undefined` | Returns CSS class names applied to a whole data row. Complements `ColDef.cellClass`. |\n| `pinRow` | `(row, index) =\u003e 'top' \\| 'bottom' \\| undefined` | `undefined` | Pins matching rows to the top/bottom of the body (see [Master/Detail and Pinned Rows](#masterdetail-and-pinned-rows)). |\n| `treeConfig` | `AgridTreeConfig\u003cT\u003e \\| null` | `null` | Builds a tree from id/parent-id accessors or `getPath` segments. Path labels and branch UUIDs can be customized with `formatPathSegment` and `nodeUuid`. |\n| `masterDetail` | `boolean` | `false` | Enables expandable detail panels. In tree mode, only leaf rows can expand details. Not available while grouped. |\n| `detailRenderer` | `(p: { row }) =\u003e string` | `undefined` | Returns sanitized HTML for an expanded detail panel. |\n| `detailRowHeight` | `number` | `200` | Fixed height in pixels of an expanded detail panel. |\n\n### Standalone tree\n\n`AgridTreeComponent` and `AgridTreeProvider` provide the same hierarchy without grid columns.\nThe control accepts `AgridTreeConfig\u003cT\u003e`, supports keyboard navigation and selection, and emits\nnormalized row/path-branch events.\n\n```ts\nreadonly treeProvider = new AgridTreeProvider\u003cNode\u003e({\n  datasource: new AgridDataSource(nodes),\n  treeConfig: {\n    getId: node =\u003e node.id,\n    getParentId: node =\u003e node.parentId,\n    treeField: 'name',\n    defaultExpanded: true,\n  },\n  getDescription: node =\u003e node.type,\n});\n```\n\n```html\n\u003cagrid-tree [provider]=\"treeProvider\" (nodeClick)=\"openNode($event)\" /\u003e\n```\n\n### Menu bar\n\nConfigure `menuBarItems` to render commands above the column headers. Main buttons and dropdown\nitems share the single `(menuBarAction)` output. `visible`, `active`, and `disabled` accept either\na boolean or a resolver receiving current rows, selected rows, selected cell, provider, and\ndatasource.\n\n```ts\nreadonly provider = new AgridProvider\u003cOrder\u003e({\n  columns,\n  datasource,\n  rowSelection: 'multi',\n  menuBarItems: [\n    { id: 'refresh', label: 'Refresh', icon: '↻' },\n    {\n      id: 'selection',\n      label: 'Selection',\n      disabled: ({ selectedRows }) =\u003e selectedRows.length === 0,\n      active: ({ selectedRows }) =\u003e selectedRows.length \u003e 0,\n      items: [\n        { id: 'approve', label: 'Approve', visible: ({ selectedRows }) =\u003e selectedRows.length \u003e 0 },\n        { id: 'archive', label: 'Archive', disabled: ({ selectedRows }) =\u003e selectedRows.some(({ row }) =\u003e row.locked) },\n      ],\n    },\n  ],\n});\n\nonMenuBarAction(id: string): void {\n  // refresh, selection, approve, archive, ...\n}\n```\n\n```html\n\u003cagrid [provider]=\"provider\" (menuBarAction)=\"onMenuBarAction($event)\" /\u003e\n```\n\n### Dynamic Provider Options\n\nThree options are `WritableSignal` properties on the provider instance — update them at runtime without recreating the provider:\n\n| Signal | Type | Description |\n| --- | --- | --- |\n| `provider.loading` | `WritableSignal\u003cboolean\u003e` | Show or hide the loading overlay. |\n| `provider.readonlyGrid` | `WritableSignal\u003cboolean\u003e` | Toggle readonly mode. |\n| `provider.autoAddRows` | `WritableSignal\u003cboolean\u003e` | Toggle automatic row insertion. |\n\nExample — toggle readonly in a host component:\n\n```ts\nreadonly provider = new AgridProvider({ ..., readonly: true });\nreadonly isEditing = signal(false);\n\nconstructor() {\n  effect(() =\u003e this.provider.readonlyGrid.set(!this.isEditing()));\n}\n```\n\nExample — server-side loading state:\n\n```ts\nasync loadPage(page: number) {\n  this.provider.loading.set(true);\n  this.ds.setData(await fetchPage(page));\n  this.provider.loading.set(false);\n}\n```\n\n### Public Component Methods\n\nCall these through `viewChild(AgridComponent)`.\n\n| Method | Description |\n| --- | --- |\n| `exportCsv(filename?)` | Downloads visible, filtered data rows as CSV using display values. Group headers are excluded. |\n| `autosizeAllColumns()` | Resizes every visible column to fit its header text and current row values. Call after setting data. |\n| `expandGroups()` | Expands every group when grouping is active. |\n| `collapseGroups()` | Collapses every group when grouping is active. |\n| `toggleSidebar()` | Opens or closes the column sidebar. |\n| `openFind()` | Opens the find panel and focuses the input. |\n| `closeFind()` | Closes the find panel. |\n| `goToFindMatch(direction)` | Moves to the next (`1`) or previous (`-1`) find match. |\n| `deleteRow(originalIndex)` | Removes a row and emits `rowRemoved`, after confirmation when `confirmRowDelete` is enabled. |\n| `clearChangedCells(originalIndex?, fields?)` | Clears every changed-cell marker, one row, or selected fields in one row. |\n| `clearMarkedRows()` | Clears all rows marked for clipboard inclusion. |\n\n### Public Component State\n\n| Property | Type | Description |\n| --- | --- | --- |\n| `selectedCell` | `Signal\u003cCellPosition \\| null\u003e` | Currently focused cell. |\n| `editingCell` | `Signal\u003cCellPosition \\| null\u003e` | Cell currently in edit mode. |\n| `selectedRowIndices` | `Signal\u003cReadonlySet\u003cnumber\u003e\u003e` | Selected original row indices. |\n| `selectedRowIndex` | `Signal\u003cnumber \\| null\u003e` | First selected row index, useful for single selection. |\n| `markedRowIndices` | `Signal\u003cReadonlySet\u003cnumber\u003e\u003e` | Original datasource indices included in copy operations. |\n| `sidebarOpen` | `Signal\u003cboolean\u003e` | Current sidebar visibility. |\n| `canUndo` | `Signal\u003cboolean\u003e` | Whether Ctrl/Cmd+Z can undo an edit. Requires `provider.control`. |\n| `canRedo` | `Signal\u003cboolean\u003e` | Whether redo is available. Requires `provider.control`. |\n| `filteredRowCount` | `Signal\u003cnumber\u003e` | Total filtered data row count, unaffected by current page. |\n| `totalPages` | `Signal\u003cnumber\u003e` | Total page count given the current filter and page size. `1` when pagination is off. |\n| `showPagination` | `Signal\u003cboolean\u003e` | Whether the pagination bar is visible (`pageSize \u003e 0`). |\n\n## Column Definitions\n\n`ColDef` describes one column.\n\nColumns, providers, datasources, and row events accept a row type. Supplying it makes\ncolumn fields and callback values type-safe:\n\n```ts\ninterface PersonRow {\n  id: number;\n  name: string;\n  active: boolean;\n}\n\nconst columns: ColDef\u003cPersonRow\u003e[] = [\n  { field: 'id', header: 'ID', formatter: value =\u003e value.toFixed(0) },\n  { field: 'name', header: 'Name', formatter: value =\u003e value.toUpperCase() },\n  {\n    field: 'active',\n    header: 'Active',\n    values: [\n      { value: true, label: 'Yes' },\n      { value: false, label: 'No' },\n    ],\n  },\n];\n\nconst datasource = new AgridDataSource\u003cPersonRow\u003e([]);\nconst provider = new AgridProvider\u003cPersonRow\u003e({ columns, datasource });\n\nfunction onRecordEdit(event: RecordEditEvent\u003cPersonRow\u003e): void {\n  console.log(event.data.name);\n}\n```\n\nAn invalid field such as `{ field: 'email' }` is rejected by TypeScript. Generic parameters\nare optional, so existing untyped configurations remain compatible.\n\n### Grouped Column Headers\n\n```ts\nconst columns: ColDef\u003cPersonRow\u003e[] = [\n  { field: 'firstName', header: 'First name', group: 'employee' },\n  { field: 'lastName', header: 'Last name', group: 'employee' },\n  { field: 'email', header: 'Email' },\n];\n\nconst provider = new AgridProvider({\n  columns,\n  headerGroups: [{ id: 'employee', label: 'Employee' }],\n});\n```\n\nThe extra header row appears when a visible column references a configured group. Only adjacent\ncolumns share one group header. Reordering, hiding, or pinning columns can split the same group ID\ninto multiple rendered segments. Dragging a group header moves every column in that segment as one\nordered block. A segment containing a locked column cannot be dragged. The `group` property is only\nfor header presentation; `groupable` continues to control data-row grouping.\n\n```ts\ninterface ColDef {\n  field: string;\n  header: string;\n  group?: string;          // references AgridProvider.headerGroups\n  width: number;           // use ColDefAutoSize (-1) to autosize on first render\n  type?: 'text' | 'number' | 'date' | 'boolean';\n  editable?: boolean;\n  cellReadonly?: (params: { value: unknown; row: Record\u003cstring, unknown\u003e; column: ColDef; originalIndex: number }) =\u003e boolean;\n  locked?: boolean;\n  values?: string[] | ValueOption[];\n  formatter?: (value: unknown) =\u003e string;\n  inputMask?: (params: { value: unknown; row: Record\u003cstring, unknown\u003e; column: ColDef }) =\u003e RegExp | null;\n  filterable?: boolean;\n  groupable?: boolean;\n  hidden?: boolean;\n  pinned?: 'left' | 'right';\n  aggregate?: 'sum' | 'avg' | 'min' | 'max' | 'count';\n  cellRenderer?: (params: { value: unknown; row: Record\u003cstring, unknown\u003e }) =\u003e string;\n  cellClass?: (params: { value: unknown; row: Record\u003cstring, unknown\u003e }) =\u003e string;\n  infoIcon?: boolean | ((params: { value: unknown; row: Record\u003cstring, unknown\u003e }) =\u003e boolean);\n}\n```\n\n| Property | Required | Description |\n| --- | --- | --- |\n| `field` | Yes | Key in each row object. |\n| `header` | Yes | Header label shown in the grid. |\n| `group` | No | Header-group ID. Adjacent columns with the same ID share a grouped header. |\n| `width` | Yes | Default width in pixels. Set to `ColDefAutoSize` (`-1`) to fit the column to its content on first render. |\n| `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. |\n| `editable` | No | Set to `false` for a read-only column. Defaults to editable. |\n| `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. |\n| `locked` | No | Prevents the column from being hidden, reordered, or unpinned through the column menu. |\n| `values` | No | Fixed editor/filter values. Use `string[]` or `{ value, label }[]`. |\n| `formatter` | No | Custom display formatter. Takes precedence over date auto-formatting. |\n| `inputMask` | No | Resolves a regular-expression input constraint for each string cell from its `row`, `value`, and `column`. Invalid proposed values are rejected. |\n| `filterable` | No | Enables text filter and value picker for the column. |\n| `groupable` | No | Enables \"group by\" in the column menu. |\n| `hidden` | No | Hides the column on first render. |\n| `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. |\n| `aggregate` | No | Shows an aggregate footer value: `'sum'`, `'avg'`, `'min'`, `'max'`, or `'count'`. |\n| `cellRenderer` | No | Custom HTML renderer. Return an HTML string; Angular sanitizes it automatically. See [Custom Cell Renderers](#custom-cell-renderers). |\n| `cellClass` | No | Returns a CSS class name for each cell. Applied alongside built-in state classes. |\n| `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. |\n\n```html\n\u003cagrid [provider]=\"provider\" (cellInfo)=\"showCellInfo($event)\" /\u003e\n```\n\n### Input masks\n\nReturn a mask per row when string values need a structured format:\n\n```ts\n{\n  field: 'reference',\n  header: 'Reference',\n  inputMask: ({ row }) =\u003e\n    row.numeric\n      ? /\\d{0,3}(?:-\\d{0,5}(?:-\\d{0,5})?)?/\n      : /[a-z0-9]{0,3}(?: [a-z0-9]{0,3}(?: [a-z0-9]{0,5})?)?/i,\n}\n```\n\nThe expression is matched against the entire proposed value, so explicit `^`\nand `$` anchors are optional. It must accept partial input, including the empty\nstring and any intermediate separators users need to type. Return `null` when\na particular row should use an unrestricted text editor.\n\n### ColDefAutoSize\n\nImport `ColDefAutoSize` and use it as the `width` value to fit the column to its content on first render:\n\n```ts\nimport { ColDefAutoSize } from './agrid';\n\nconst columns: ColDef[] = [\n  { field: 'name', header: 'Name', width: ColDefAutoSize },\n  { field: 'email', header: 'Email', width: ColDefAutoSize },\n];\n```\n\nThe column sizes itself once on first render and then behaves like a normal resizable column.\n\n### Value Options\n\nUse value options when stored values differ from labels.\n\n```ts\ninterface ValueOption {\n  value: unknown;\n  label: string;\n}\n```\n\nExample:\n\n```ts\n{\n  field: 'departmentId',\n  header: 'Department',\n  width: 140,\n  values: [\n    { value: 1, label: 'Engineering' },\n    { value: 2, label: 'Sales' },\n  ],\n}\n```\n\nThe grid displays labels, but committed edits store `value`.\n\n## Date Auto-Formatting\n\nThe grid automatically detects and formats date values without any configuration. Both display and sorting use the native date value.\n\n**Auto-detected formats:**\n- `Date` objects\n- ISO 8601 strings: `\"2024-01-15\"`, `\"2024-01-15T10:30:00Z\"`, `\"2024-01-15T10:30:00+02:00\"`\n\n**Display:** Values are formatted using the browser's locale — e.g. `Jan 15, 2024`.\n\n**Sorting:** Date columns sort chronologically by raw timestamp, not alphabetically by display string.\n\n**Priority:** `values` list → `formatter` → date auto-format → raw string.\n\nTo force date formatting on a column regardless of value shape, set `type: 'date'`.\n\nTo use a custom date format, set `formatter`:\n\n```ts\n{ field: 'hiredAt', header: 'Hired', width: 120,\n  formatter: v =\u003e new Date(v as string).toLocaleDateString('de-DE') }\n```\n\n## Zebra Stripes\n\nAlternating row shading is opt-in via the provider:\n\n```ts\nreadonly provider = new AgridProvider({ ..., zebraStripes: true });\n```\n\nOverride the stripe color with a CSS custom property on the host:\n\n```css\nagrid {\n  --agrid-color-bg-stripe: #f0f4ff;\n}\n```\n\nHover and selection colors always override the stripe.\n\n## Readonly Mode\n\nSet `readonly: true` in the provider to make the entire grid non-editable:\n\n```ts\nreadonly provider = new AgridProvider({ ..., readonly: true });\n```\n\nTo toggle readonly at runtime, use the `readonlyGrid` signal on the provider:\n\n```ts\nreadonly isReadonly = signal(true);\n\nconstructor() {\n  effect(() =\u003e this.provider.readonlyGrid.set(this.isReadonly()));\n}\n```\n\nIndividual `ColDef.editable: false` still works when `readonly` is `false`.\n\n## Pagination\n\nPagination 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.\n\n```ts\nreadonly gridControl = new AgridControl({ pageSize: 25 });\n```\n\nOr change it at runtime:\n\n```ts\nthis.gridControl.setPageSize(10);  // 0 = show all rows\nthis.gridControl.setPage(2);\n```\n\nPagination applies to data rows after filtering and sorting, before grouping. Each page therefore always contains at most `pageSize` data rows.\n\n### Server-side filtering and sorting\n\nEnable `serverSideFiltering` when the API should filter and sort the dataset:\n\n```ts\nreadonly provider = new AgridProvider({\n  columns: [\n    { field: 'name', header: 'Name', filterable: true },\n    { field: 'status', header: 'Status', filterable: true },\n  ],\n  datasource: this.ds,\n  control: this.ctrl,\n  serverSideFiltering: true,\n  sortOption: 'single',\n});\n```\n\n```html\n\u003cagrid\n  [provider]=\"provider\"\n  (filterChange)=\"onFilter($event)\"\n  (sortChange)=\"onSort($event)\"\n/\u003e\n```\n\n```ts\nreadonly filters = new Map\u003cstring, string\u003e();\nreadonly sorts = new Map\u003cstring, 'asc' | 'desc'\u003e();\n\nonFilter(event: FilterChangeEvent): void {\n  if (event.value) this.filters.set(event.field, event.value);\n  else this.filters.delete(event.field);\n  this.loadRows();\n}\n\nonSort(event: SortChangeEvent): void {\n  if (event.direction) this.sorts.set(event.field, event.direction);\n  else this.sorts.delete(event.field);\n  this.loadRows();\n}\n```\n\nIn server-side mode:\n\n- Filter and sort state remains visible in the grid headers.\n- The grid does not filter or sort loaded rows locally.\n- The Excel-style distinct-value checklist is hidden.\n- Clearing emits an empty filter value or a `null` sort direction.\n- Multi-column sorting emits one event for each changed column.\n- Text filter events are debounced by `filterDebounceMs` (300 ms by default).\n\nUse `sortOption: 'single'` for backends that accept only one sort field. Selecting another column\nclears the previous sort first. Use `'none'` to remove sorting controls completely; `'multi'`\npreserves the default multi-column behavior.\n\nThe grid updates its visible filter state immediately, but only emits the final server filter value\nafter the debounce delay. Set `filterDebounceMs: 0` when immediate events are required. For server\npagination, call `control.setTotalRows(total)` after each response and replace the datasource\ncontents with the returned page.\n\n## Custom Cell Renderers\n\nReturn an HTML string from `cellRenderer` to render rich content in a cell. Angular's built-in\nsanitization runs automatically. Use CSS classes rather than inline styles; Angular strips unsafe\nattributes and logs a development warning when renderer output requires sanitization. Escape\ndynamic text before interpolating it into HTML.\n\n```ts\nconst columns: ColDef[] = [\n  {\n    field: 'status',\n    header: 'Status',\n    width: 100,\n    editable: false,\n    cellRenderer: ({ value }) =\u003e {\n      const status = value === 'active' ? 'active' : 'inactive';\n      return `\u003cspan class=\"status-badge status-badge--${status}\"\u003e${status}\u003c/span\u003e`;\n    },\n  },\n  {\n    field: 'salary',\n    header: 'Salary',\n    width: 120,\n    editable: false,\n    cellRenderer: ({ value, row }) =\u003e\n      `\u003cstrong\u003e$${Number(value).toLocaleString()}\u003c/strong\u003e`,\n  },\n];\n```\n\nThe `row` parameter gives you access to the full row object, useful when the display depends on sibling fields.\n\n## Column Autosize\n\nFit all visible columns to their content after loading data:\n\n```ts\nconstructor() {\n  afterNextRender(() =\u003e this._grid()?.autosizeAllColumns());\n}\n```\n\nOr autosize a single column by double-clicking its resize handle, or through the column menu.\n\n## AgridDataSource\n\n`AgridDataSource\u003cT\u003e` is a signal-based row container shared by the host and grid.\n\n```ts\nconst ds = new AgridDataSource\u003cRecord\u003cstring, unknown\u003e\u003e([\n  { id: 1, name: 'Alice' },\n]);\n```\n\n### Linking an Angular signal\n\nLink a writable Angular signal directly when the application and grid should share ownership of\nthe rows:\n\n```ts\ninterface Row {\n  id: number;\n  name: string;\n}\n\nreadonly rows = signal\u003cRow[]\u003e([\n  { id: 1, name: 'Alice' },\n]);\nreadonly ds = new AgridDataSource\u003cRow\u003e();\nreadonly provider = new AgridProvider({\n  columns: [\n    { field: 'id', header: 'ID', editable: false },\n    { field: 'name', header: 'Name' },\n  ],\n  datasource: this.ds,\n});\n\nconstructor() {\n  this.ds.linkSignal(this.rows);\n}\n```\n\nNo synchronization `effect()` is needed. Updates work in both directions:\n\n- Calling `rows.set(...)` or `rows.update(...)` refreshes the grid.\n- Cell edits, paste, `setData`, `updateRow`, `patchRow`, `addRow`, `removeRow`, and `moveRow`\n  update `rows` automatically.\n- Undo and redo also update `rows` because they use datasource mutations.\n\nThe `(cellEdit)` output is not required to keep the writable signal synchronized. Use it only for\nside effects such as saving changes to an API:\n\n```html\n\u003cagrid [provider]=\"provider\" (cellEdit)=\"saveEdit($event)\" /\u003e\n```\n\nFor one-way linking, pass a readonly signal:\n\n```ts\nreadonly rows = signal\u003cRow[]\u003e([]);\n\nconstructor() {\n  this.ds.linkSignal(this.rows.asReadonly());\n}\n```\n\nIn this mode, source updates refresh the grid, but grid mutations remain local to the datasource.\nIn both modes, source updates are linked without copying the source array.\n\n| Member | Description |\n| --- | --- |\n| `rows` | Readonly Angular `Signal\u003cT[]\u003e` of current rows. |\n| `linkSignal(source)` | Links an external signal without copying. Writable signals receive datasource mutations automatically. |\n| `setData(rows)` | Replaces all rows with a shallow copy. |\n| `updateRow(index, row)` | Replaces one row. |\n| `patchRow(index, patch)` | Merges a partial row update. |\n| `addRow(row, atIndex?)` | Inserts a row and returns the inserted index. |\n| `removeRow(index)` | Removes a row. |\n| `moveRow(from, to)` | Moves a row using insert-before semantics. |\n| `getRow(index)` | Returns a non-reactive row snapshot. |\n| `length` | Current row count. |\n\n## AgridControl\n\n`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.\n\n```ts\nconst control = new AgridControl({\n  allowRowReorder: true,\n  hiddenColumns: ['salary'],\n  pinnedColumns: ['id'],\n  pageSize: 20,\n});\n```\n\n### Control State\n\n```ts\ninterface AgridControlState {\n  columnWidths: Record\u003cstring, number\u003e;\n  filters: Record\u003cstring, ColumnFilter\u003e;\n  allowRowReorder?: boolean;\n  groupByField?: string | null;\n  hiddenColumns?: string[];\n  columnOrder?: string[];\n  pinnedColumns?: string[];\n  pageSize?: number;\n  currentPage?: number;\n}\n```\n\n### Column Filters\n\n```ts\ninterface ColumnFilter {\n  text: string;\n  selectedValues: string[] | null;\n  sort: 'asc' | 'desc' | null;\n}\n```\n\n`text`, `selectedValues`, and `sort` are combined when rows are displayed. `selectedValues: null` means all values are allowed.\n\n### Control Signals\n\n| Signal | Description |\n| --- | --- |\n| `allowRowReorder` | Whether row drag handles can reorder rows. |\n| `groupByField` | Field currently used for grouping, or `null`. |\n| `hiddenColumns` | Set of hidden field names. |\n| `columnOrder` | Current field order. Empty means original `colDefs` order. |\n| `pinnedColumns` | Set of pinned field names. |\n| `columnWidths` | Width overrides by field. |\n| `filters` | Active filter/sort state by field. |\n| `pageSize` | Rows per page. `0` means all rows (no pagination). |\n| `currentPage` | Current page number (1-based). |\n| `canUndo` | Whether an undo history item exists. |\n| `canRedo` | Whether a redo history item exists. |\n\n### Control Methods\n\n| Method | Description |\n| --- | --- |\n| `setAllowRowReorder(value)` | Enables or disables row reorder. |\n| `setGroupBy(field)` | Groups by a field or clears grouping with `null`. |\n| `isColumnHidden(field)` | Returns whether a column is hidden. |\n| `setColumnVisibility(field, visible)` | Shows or hides a column. |\n| `toggleColumnVisibility(field)` | Toggles column visibility. |\n| `setColumnOrder(fields)` | Replaces the current column order. |\n| `moveColumn(currentVisibleOrder, fromField, toField, insertBefore)` | Reorders columns. Used by header dragging. |\n| `isPinned(field)` | Returns whether a column is pinned. |\n| `setPinned(field, pinned)` | Pins or unpins a column. |\n| `togglePinned(field)` | Toggles pinning. |\n| `getColumnWidth(field, defaultWidth)` | Returns effective width. |\n| `setColumnWidth(field, width)` | Sets a width override with a 40 px minimum. |\n| `getFilter(field)` | Returns current filter state or defaults. |\n| `setTextFilter(field, text)` | Sets text filter. |\n| `setSelectedValues(field, values)` | Sets allowed values, or `null` for all. |\n| `setSort(field, sort)` | Sets sort and clears sort on other fields. |\n| `clearFilter(field)` | Clears one column filter/sort. |\n| `clearAllFilters()` | Clears all filters and sorts. |\n| `hasActiveFilter(field)` | Returns whether a column has active filter/sort state. |\n| `hasAnyActiveFilter()` | Returns whether any column has active filter/sort state. |\n| `setPageSize(size)` | Sets rows per page. `0` disables pagination. Resets to page 1. |\n| `setPage(page)` | Navigates to a page (1-based). Clamped to valid range by the grid. |\n| `pushEdit(entry)` | Adds one edit to undo history. Used by the grid. |\n| `pushEditBatch(entries)` | Adds a multi-cell operation as one undo step. Used by paste/fill. |\n| `undo()` | Returns a `HistoryItem` to reverse, or `null`. The grid applies it. |\n| `redo()` | Returns a `HistoryItem` to reapply, or `null`. The grid applies it. |\n| `clearHistory()` | Clears undo/redo history. |\n| `toJSON()` | Serializes control state including pagination. |\n| `AgridControl.fromJSON(state)` | Restores control state. |\n\n## Events And Types\n\n### GridEditEvent\n\n```ts\ninterface GridEditEvent {\n  position: CellPosition;\n  field: string;\n  oldValue: unknown;\n  newValue: unknown;\n}\n```\n\nEmitted whenever a committed grid operation changes a cell.\n\n### FilterChangeEvent\n\n```ts\ninterface FilterChangeEvent {\n  field: string;\n  value: string;\n}\n```\n\nAn empty `value` clears the server-side text filter.\n\n### SortChangeEvent\n\n```ts\ninterface SortChangeEvent {\n  field: string;\n  direction: 'asc' | 'desc' | null;\n}\n```\n\nA `null` direction clears the server-side sort for that field.\n\n### CellPosition\n\n```ts\ninterface CellPosition {\n  rowIndex: number;\n  colIndex: number;\n}\n```\n\n`rowIndex` is the original data-source row index. `colIndex` is the visible column index.\n\n### RowSelectEvent\n\n```ts\ninterface RowSelectEvent {\n  rows: { row: Record\u003cstring, unknown\u003e; originalIndex: number }[];\n}\n```\n\n`rowSelect` emits `null` when selection is cleared.\n\n### RowReorderEvent\n\n```ts\ninterface RowReorderEvent {\n  row: Record\u003cstring, unknown\u003e;\n  oldIndex: number;\n  newIndex: number;\n}\n```\n\nThe grid does not reorder rows itself on drop. Call `dataSource.moveRow(event.oldIndex, event.newIndex)` in the handler.\n\n### NewRecord\n\n```ts\ninterface NewRecord {\n  index: number;\n  data: Record\u003cstring, unknown\u003e;\n  provider: AgridProvider;\n  datasource: AgridDataSource;\n}\n```\n\nFor repeated grids, use the source carried by the event instead of looking the provider up by\nthe row or loop index:\n\n```ts\nonPrepareAdd(event: NewRecord): void {\n  const next = event.datasource.length;\n  event.datasource.patchRow(event.index, { id: next, departmentId: 1 });\n}\n```\n\nEmitted after the grid inserts a blank row. Patch defaults from the host if needed.\n\n### RowUpdateEvent\n\n```ts\ninterface RowUpdateEvent\u003cT extends object = Record\u003cstring, unknown\u003e\u003e {\n  row: T;\n  originalIndex: number;\n}\n```\n\n`rowChanged` carries the latest complete datasource row and its current zero-based index. Inline\nedits are grouped until the active row is left. Sidebar-only editing emits the same event when the\nSave button is clicked.\n\n### GroupAction\n\n```ts\ninterface GroupAction {\n  label: string;\n  action: (groupLabel: string) =\u003e void;\n}\n```\n\nActions appear in group header menus.\n\n### HistoryEntry And HistoryItem\n\n```ts\ninterface HistoryEntry {\n  rowIndex: number;\n  field: string;\n  oldValue: unknown;\n  newValue: unknown;\n}\n\ntype HistoryItem = HistoryEntry | HistoryEntry[];\n```\n\nPaste and fill store multiple entries as one `HistoryItem`, so Ctrl/Cmd+Z reverses the whole operation.\n\n## Keyboard And Mouse Behavior\n\n| Action | Behavior |\n| --- | --- |\n| Arrow keys | Move active cell. |\n| Shift+arrow | Extend cell range selection. |\n| Tab / Shift+Tab | Move right / left, wrapping rows. |\n| Enter | Start editing the active cell. |\n| Enter while editing | Commit and follow `enterEditAction` (`nextRow` by default). |\n| Ctrl/Cmd+Enter | Toggle an expandable tree node. |\n| F2 | Start editing active cell. |\n| Printable key | Start editing active cell with typed seed character. |\n| Escape | Close any open menu, cancel edit, or close find when its input is focused. |\n| Ctrl/Cmd+Z | Undo. |\n| Ctrl/Cmd+Y | Redo. |\n| Ctrl/Cmd+Shift+Z | Redo. |\n| Ctrl/Cmd+F | Open find panel. |\n| Enter in find | Next match. |\n| Shift+Enter in find | Previous match. |\n\nOpening find clears the active cell so typing remains in the find input. Tree searches include\ncollapsed descendants; navigating to one expands its ancestor path before scrolling to the match.\n| Click cell | Select cell. |\n| Shift+click cell | Extend range selection. |\n| Double-click cell | Start editing. |\n| Drag fill handle | Fill selected value/range down or right. |\n| Double-click resize handle | Autosize column. |\n| Drag resize handle | Resize column. |\n| Drag header | Reorder columns when `control` is provided. |\n| Right-click control cell | Open row context menu. |\n\n## Filtering, Sorting, And Grouping\n\n- A filter row appears when at least one visible column has `filterable: true`.\n- Text filter and value picker are combined.\n- Sort is single-column. Setting sort on one field clears sort on other fields.\n- Date columns sort chronologically by raw value, not alphabetically by display string.\n- Grouping is enabled per column with `groupable: true`.\n- Group state is controlled through `AgridControl.setGroupBy(field | null)`.\n- `expandGroups()` and `collapseGroups()` can be called on the component.\n\n## Clipboard, Range Selection, And Fill\n\n- Copy exports the active cell or selected rectangular range as TSV.\n- With `enableRowMarking`, checked rows are appended to every copy using the copied columns.\n- Copying without an active cell copies all visible columns from the marked rows.\n- Context-menu `Copy cell` and `Copy row` also include marked rows without duplicates.\n- Row marking is independent from row selection.\n- Marked rows remain part of copy output when filters hide them.\n- Paste accepts TSV or CSV-like plain text and writes from the active cell.\n- Pasted values use labels/raw values for `values` columns.\n- Number columns coerce numeric pasted values to `number`.\n- Paste skips read-only columns.\n- Fill repeats the selected source block into the dragged target area.\n- Paste and fill are each one undo history item.\n\n## Master/Detail and Pinned Rows\n\n### Master/detail\n\nSet `masterDetail: true` and provide a `detailRenderer` to make every data row expandable. A chevron\nappears in the control column; clicking it reveals a detail panel rendered beneath the row. The\nrenderer returns an HTML string (sanitized automatically, like `cellRenderer`).\n\n```ts\nreadonly provider = new AgridProvider\u003cOrder\u003e({\n  columns, datasource,\n  masterDetail: true,\n  detailRowHeight: 160, // fixed panel height in px (default 200)\n  detailRenderer: ({ row }) =\u003e `\u003cdiv class=\"order-detail\"\u003e${row.notes}\u003c/div\u003e`,\n});\n```\n\nDetail panels are sized by a built-in variable-height virtual-scroll strategy, so large lists stay\nperformant whether or not panels are open. In tree mode, only leaf rows expose detail panels;\nparent rows continue to control tree expansion. Master/detail remains disabled while grouping.\nToggle a panel imperatively with the public\n`toggleDetail(originalIndex)` / `isDetailExpanded(originalIndex)` methods on the component.\n\n### Pinned rows\n\n`pinRow` designates rows to keep fixed at the top or bottom of the body during vertical scroll —\nideal for header or total/summary rows. Pinned rows are pulled out of grouping and pagination but\nkeep their real data-source index, so editing, selection, and cell rendering work on them unchanged.\n\n```ts\nreadonly provider = new AgridProvider\u003cOrder\u003e({\n  columns, datasource, // datasource includes a summary row\n  pinRow: row =\u003e (row.isSummary ? 'bottom' : undefined),\n});\n```\n\n**Interactive pinning.** Right-click any row (its cell context menu, or the control-cell row menu)\nto **Pin row to top / bottom** or **Unpin row**. A runtime override always wins over the `pinRow`\npredicate, so a user can unpin a declaratively-pinned row. Drive it programmatically with the\npublic component methods `pinRowTo(originalIndex, 'top' | 'bottom' | null)` and\n`rowPinState(originalIndex)`.\n\n\u003e Pinned rows are designated over existing data-source rows (not a separate detached array).\n\u003e Keyboard arrow-navigation and range-selection do not currently cross the body↔pinned boundary.\n\n### Row CSS classes\n\n`getRowClass` returns class names for a whole data row, complementing the per-cell `ColDef.cellClass`:\n\n```ts\ngetRowClass: ({ row }) =\u003e (row.status === 'overdue' ? 'row-danger' : '')\n```\n\n## Pinned Columns\n\nPinned 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.\n\nPin columns initially with:\n\n```ts\n{ field: 'id', header: 'ID', width: 70, pinned: 'left' }\n```\n\nOr at runtime:\n\n```ts\ncontrol.setPinned('id', true);\n```\n\n## State Persistence\n\n```ts\nconst saved = localStorage.getItem('agrid-state');\nconst control = AgridControl.fromJSON(saved ? JSON.parse(saved) : {});\n\nlocalStorage.setItem('agrid-state', JSON.stringify(control.toJSON()));\n```\n\nPersisted state includes widths, filters, sort, grouping, hidden columns, column order, pinned columns, row reorder setting, page size, and current page.\n\n## Layout In A Card Or Flex Container\n\nThe grid host is a flex column. Give it a defined height by participating in the parent's flex layout:\n\n```css\n/* Angular Material card example */\nmat-card {\n  height: 600px;\n  display: flex;\n  flex-direction: column;\n}\n\nmat-card-content {\n  flex: 1;\n  min-height: 0;\n  display: flex;\n  flex-direction: column;\n  padding: 0;\n}\n\nagrid {\n  flex: 1;\n  min-height: 0;\n}\n```\n\n## CSS Custom Properties\n\nOverride these on the `agrid` host element to theme the grid.\n\n| Property | Default | Description |\n| --- | --- | --- |\n| `--agrid-color-text` | `#24292f` | Primary text color. Also used by `agrid-tree`. |\n| `--agrid-color-text-muted` | `#57606a` | Secondary / placeholder text. Also used by `agrid-tree`. |\n| `--agrid-color-accent` | `#1a73e8` | Selection, focus, and active state color. |\n| `--agrid-color-border` | `#d0d7de` | Cell and header borders. |\n| `--agrid-color-bg` | `#ffffff` | Cell background. |\n| `--agrid-color-bg-subtle` | `#fafbfc` | Control column background. |\n| `--agrid-color-bg-muted` | `#f6f8fa` | Header and hover background. |\n| `--agrid-color-bg-stripe` | `#f0f2f5` | Zebra stripe background (even rows). |\n| `--agrid-color-cell-changed` | `#f59e0b` | Corner marker for changed cells. |\n| `--agrid-color-row-marked` | `#fff8c5` | Background for rows marked for clipboard inclusion. |\n\n## Development\n\n```bash\npnpm install\npnpm start\npnpm build          # publishable package\npnpm build:demo\npnpm copy:local     # uncompiled runtime sources in localdist/agrid\npnpm test\npnpm test:e2e\npnpm test:performance\n```\n\nThe TypeScript compile check:\n\n```bash\n./node_modules/.bin/tsc --noEmit -p tsconfig.app.json\n```\n\nThe Playwright suite starts the Angular demo server automatically and runs the grid interaction\ntests in Chromium. Install its browser once when setting up a new environment:\n\n```bash\npnpm exec playwright install chromium\n```\n\n`pnpm test:performance` runs the isolated large-dataset suite serially against 10k, 50k, 100k,\nand 250k rows. It reports initial render, filtering, sorting, grouping, aggregation, row updates,\nand virtual-scroll timings without enforcing machine-dependent thresholds. The same operations can\nbe run manually at `/performance`.\n\n`pnpm build:lib` increments the package patch version and creates the publishable Angular package\nin `dist/agrid-package`. Inspect the package\ncontents with:\n\n```bash\ncd dist/agrid-package\nnpm pack --dry-run\n```\n\n`pnpm copy:local` recreates `localdist/agrid` with only the library's runtime `.ts`, `.html`, and\n`.css` files. Tests, documentation, licenses, package metadata, and build configuration are\nexcluded, making the directory suitable for source-level debugging in another Angular workspace.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthkl%2Fagrid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthkl%2Fagrid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthkl%2Fagrid/lists"}