infi-grid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +654 -0
- package/fesm2022/infi-grid.mjs +10314 -0
- package/fesm2022/infi-grid.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/components/cell-editor.component.d.ts +42 -0
- package/lib/components/filter-popup.component.d.ts +89 -0
- package/lib/components/filter-row.component.d.ts +104 -0
- package/lib/components/grid.component.d.ts +155 -0
- package/lib/components/header-row.component.d.ts +76 -0
- package/lib/components/row.component.d.ts +106 -0
- package/lib/components/summary-row.component.d.ts +40 -0
- package/lib/components/tooltip-controller.d.ts +43 -0
- package/lib/components/viewport.component.d.ts +217 -0
- package/lib/core/accessor.d.ts +6 -0
- package/lib/core/config-resolver.d.ts +16 -0
- package/lib/core/defaults.d.ts +7 -0
- package/lib/core/filter-engine.d.ts +44 -0
- package/lib/core/format.d.ts +11 -0
- package/lib/core/keys.d.ts +24 -0
- package/lib/core/row-ops.d.ts +20 -0
- package/lib/core/sort-engine.d.ts +11 -0
- package/lib/core/summary-engine.d.ts +35 -0
- package/lib/core/template-registry.d.ts +27 -0
- package/lib/core/theme.d.ts +2 -0
- package/lib/core/tooltip-position.d.ts +24 -0
- package/lib/core/values.d.ts +21 -0
- package/lib/core/virtual-math.d.ts +57 -0
- package/lib/directives/grid-template-component.d.ts +16 -0
- package/lib/directives/outlet.directive.d.ts +25 -0
- package/lib/directives/template.directives.d.ts +98 -0
- package/lib/grid.module.d.ts +15 -0
- package/lib/models/api.types.d.ts +249 -0
- package/lib/models/column.types.d.ts +184 -0
- package/lib/models/config.types.d.ts +390 -0
- package/lib/models/crud.types.d.ts +179 -0
- package/lib/models/data.types.d.ts +41 -0
- package/lib/models/events.types.d.ts +98 -0
- package/lib/models/filter.types.d.ts +78 -0
- package/lib/models/index.d.ts +12 -0
- package/lib/models/selection.types.d.ts +173 -0
- package/lib/models/sort.types.d.ts +22 -0
- package/lib/models/summary.types.d.ts +78 -0
- package/lib/models/template.types.d.ts +126 -0
- package/lib/models/tooltip.types.d.ts +48 -0
- package/lib/state/filter-popup.service.d.ts +30 -0
- package/lib/state/grid-api.d.ts +4 -0
- package/lib/state/grid-store.d.ts +584 -0
- package/lib/state/tooltip.service.d.ts +38 -0
- package/package.json +41 -0
- package/public-api.d.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
# InfiGrid
|
|
2
|
+
|
|
3
|
+
A fast, configurable data grid for **Angular 19+**.
|
|
4
|
+
|
|
5
|
+
- **Large data without lag.** Row and column virtualization with reused DOM, and no blank rows or columns even when scrolling fast. Tested at 50,000 to 1,000,000 rows and up to 200 columns at a steady 60 fps.
|
|
6
|
+
- **Excel-style filtering.** Typed conditions (text, number, date, boolean; up to 5, AND/OR) plus a searchable checkbox list of values, with Apply / Cancel / Clear filter / Clear all. Optional filter row under the header.
|
|
7
|
+
- **Sorting.** Ascending/descending, multi-column sort with order numbers, and configurable sort icons.
|
|
8
|
+
- **Row selection.** Single or multiple, by click, checkbox column (with select-all) or keyboard; identity by primary key, so it survives sorting, filtering, refreshes and virtualization.
|
|
9
|
+
- **Cell selection.** One selected cell by click or arrow keys, independent of row selection, with an optional active row.
|
|
10
|
+
- **Add, update and delete.** API and UI (action column, inline add row, Delete key), required fields, unique keys, row validation, delete confirmation, and cancelable before-events. **Batch mode** keeps changes pending (marked rows, undo/redo) until you save them.
|
|
11
|
+
- **Editing.** Display and edit templates, and built-in text, number, dropdown, checkbox and date editors. Also custom editor components, validation, and cell, row or whole-grid edit modes.
|
|
12
|
+
- **Tooltips** for cut-off cell and header text (or always, or custom templates), with delays and placement.
|
|
13
|
+
- **Summaries.** Sum, Average, Count, Min, Max, or custom functions and formulas. The summary row can be pinned (sticky) or a normal last row.
|
|
14
|
+
- **Templates.** Cell, header, edit, summary, loading, empty-data and error templates. Each can be an `ng-template` or an Angular component.
|
|
15
|
+
- **Grid API and events** for everything, plus public TypeScript types.
|
|
16
|
+
- **Themeable** with CSS variables, including a dark theme. No console output, and no dependencies beyond Angular.
|
|
17
|
+
|
|
18
|
+
> The project source also includes a full developer guide (`docs/GUIDE.md`): architecture, data flow, every option and performance details.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Contents
|
|
23
|
+
|
|
24
|
+
1. [Install](#install)
|
|
25
|
+
2. [Quick start](#quick-start)
|
|
26
|
+
3. [Columns](#columns)
|
|
27
|
+
4. [Configuration and feature flags](#configuration-and-feature-flags)
|
|
28
|
+
5. [Templates](#templates)
|
|
29
|
+
6. [Editing](#editing)
|
|
30
|
+
7. [Row identity and selection](#row-identity-and-selection)
|
|
31
|
+
8. [Cell selection](#cell-selection)
|
|
32
|
+
9. [Add, update and delete](#add-update-and-delete)
|
|
33
|
+
10. [Batch editing](#batch-editing)
|
|
34
|
+
11. [Filtering](#filtering)
|
|
35
|
+
12. [Sorting](#sorting)
|
|
36
|
+
13. [Summaries](#summaries)
|
|
37
|
+
14. [Tooltips](#tooltips)
|
|
38
|
+
15. [Loading, empty and error states](#loading-empty-and-error-states)
|
|
39
|
+
16. [Data refresh](#data-refresh)
|
|
40
|
+
17. [Grid API](#grid-api)
|
|
41
|
+
18. [Events](#events)
|
|
42
|
+
19. [Styling and theming](#styling-and-theming)
|
|
43
|
+
20. [Keyboard](#keyboard)
|
|
44
|
+
21. [Performance tips](#performance-tips)
|
|
45
|
+
22. [Notes and limitations](#notes-and-limitations)
|
|
46
|
+
23. [Coming from angpackage-grid](#coming-from-angpackage-grid)
|
|
47
|
+
24. [Compatibility](#compatibility)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install infi-grid
|
|
55
|
+
|
|
56
|
+
# or, without a registry, from the packed file
|
|
57
|
+
npm install ./infi-grid-1.0.0.tgz
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Peer dependencies: `@angular/core` and `@angular/common` 19 or newer. There's nothing else to install, and no global stylesheet to add (styles ship with the component).
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { Component, signal } from '@angular/core';
|
|
66
|
+
import { INFI_GRID, type ColumnConfig, type GridApi, type GridConfig } from 'infi-grid';
|
|
67
|
+
|
|
68
|
+
interface Order {
|
|
69
|
+
id: number;
|
|
70
|
+
customer: string;
|
|
71
|
+
amount: number;
|
|
72
|
+
orderDate: Date;
|
|
73
|
+
shipped: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Component({
|
|
77
|
+
selector: 'app-orders',
|
|
78
|
+
imports: [INFI_GRID],
|
|
79
|
+
template: `
|
|
80
|
+
<infi-grid
|
|
81
|
+
style="height: 600px"
|
|
82
|
+
[data]="orders()"
|
|
83
|
+
[columns]="columns"
|
|
84
|
+
[config]="config"
|
|
85
|
+
(gridReady)="api = $event.api"
|
|
86
|
+
(cellValueChange)="save($event.row)"
|
|
87
|
+
/>
|
|
88
|
+
`,
|
|
89
|
+
})
|
|
90
|
+
export class OrdersComponent {
|
|
91
|
+
readonly orders = signal<Order[]>([]);
|
|
92
|
+
api?: GridApi<Order>;
|
|
93
|
+
|
|
94
|
+
readonly columns: ColumnConfig<Order>[] = [
|
|
95
|
+
{ field: 'id', header: 'ID', type: 'number', width: 90 },
|
|
96
|
+
{ field: 'customer', header: 'Customer', editable: true },
|
|
97
|
+
{ field: 'amount', header: 'Amount', type: 'number', numberFormat: { style: 'currency', currency: 'USD' }, summary: 'sum' },
|
|
98
|
+
{ field: 'orderDate', header: 'Order date', type: 'date', editable: true },
|
|
99
|
+
{ field: 'shipped', header: 'Shipped', type: 'boolean', editable: true },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
readonly config: GridConfig<Order> = {
|
|
103
|
+
sorting: { initialSort: [{ field: 'orderDate', direction: 'desc' }] },
|
|
104
|
+
summary: { sticky: true },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
save(order: Order) {
|
|
108
|
+
/* persist the change */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**The grid needs a height.** Set it with CSS (`infi-grid { height: 100% }`, `style="height: 600px"`, flex, and so on); the default is 400px.
|
|
114
|
+
|
|
115
|
+
For NgModule-based apps, import `InfiGridModule` instead of `INFI_GRID`.
|
|
116
|
+
|
|
117
|
+
## Columns
|
|
118
|
+
|
|
119
|
+
Columns are an array of `ColumnConfig`. Every behaviour flag on a column overrides the matching global flag.
|
|
120
|
+
|
|
121
|
+
| Property | Purpose |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `field` | Property name or dot path (`'customer.name'`). Unique. |
|
|
124
|
+
| `header` | Header text (defaults to the field). |
|
|
125
|
+
| `type` | `'text'` (default), `'number'`, `'date'`, `'boolean'`. Drives formatting, sorting, filtering, editors and summaries. |
|
|
126
|
+
| `width`, `minWidth`, `maxWidth` | Pixels. Default width 150, min 48. Columns can be drag-resized within min/max. |
|
|
127
|
+
| `sortable`, `filterable`, `editable` | Column-level flags. `editable` can be a function `(row) => boolean`. |
|
|
128
|
+
| `resizable`, `hidden`, `align` | Layout. Numbers are right-aligned by default. |
|
|
129
|
+
| `displayTemplate`, `editTemplate`, `headerTemplate`, `summaryTemplate` | `TemplateRef` or component class. |
|
|
130
|
+
| `templateKey` | Reuse key of the column's template cells: a cell scrolled out of view is reused for a column with the same key, so it only updates. Use the column's type when one template renders different content (an `@switch`). |
|
|
131
|
+
| `cellClass`, `cellStyle` | Static value or `(ctx) => …` for per-cell styling. |
|
|
132
|
+
| `headerClass`, `headerStyle` | Header styling. |
|
|
133
|
+
| `summary` | `'sum'`, `'avg'`, `'count'`, `'min'`, `'max'`, or a `ColumnSummaryConfig` with a custom `fn`. |
|
|
134
|
+
| `sort` | `{ comparator, showIcon, iconVisibility, cycle }` |
|
|
135
|
+
| `filter` | `{ operators, defaultOperator, customOperators, valueList, conditions, maxConditions, caseSensitive, predicate, showIcon, iconVisibility }` |
|
|
136
|
+
| `valueGetter`, `valueSetter` | Computed (formula) columns, and writing edits back. |
|
|
137
|
+
| `valueFormatter`, `numberFormat`, `dateFormat`, `booleanLabels` | Display formatting. |
|
|
138
|
+
| `editor`, `editorOptions`, `validator` | Editing (see below). |
|
|
139
|
+
| `required`, `defaultValue`, `editableOnAdd` | Adding rows (see [Add, update and delete](#add-update-and-delete)). |
|
|
140
|
+
| `tooltip`, `tooltipTemplate`, `description`, `headerTooltip`, `headerTooltipTemplate` | Tooltips (see [Tooltips](#tooltips)). |
|
|
141
|
+
| `meta` | Anything you want to read in your own templates. |
|
|
142
|
+
|
|
143
|
+
Use `defaultColumn` in the config to set defaults for all columns:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
config = { defaultColumn: { width: 120, resizable: true } };
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Configuration and feature flags
|
|
150
|
+
|
|
151
|
+
Every section is optional. Defaults are shown.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const config: GridConfig<Order> = {
|
|
155
|
+
primaryKey: 'id', // field, dot path or (row) => key; identifies rows (selection, CRUD, refresh)
|
|
156
|
+
dataRefresh: { filter: 'keep', sort: 'keep', selection: 'keep', scroll: 'keep' }, // what a refresh keeps ('reset' / 'top')
|
|
157
|
+
selection: {
|
|
158
|
+
mode: 'none', // 'single' | 'multiple'
|
|
159
|
+
checkbox: undefined, // default: true in 'multiple' mode
|
|
160
|
+
checkboxWidth: 40, headerCheckbox: true,
|
|
161
|
+
selectOnClick: true, // ignored while cellSelection.enabled is on
|
|
162
|
+
selectAllScope: 'filtered', // 'all'
|
|
163
|
+
onFilter: 'deselectHidden', // 'clear' | 'keep'
|
|
164
|
+
scrollToSelection: false, keyboard: true, rowSelectable: undefined,
|
|
165
|
+
},
|
|
166
|
+
cellSelection: { enabled: false, activeRow: false }, // one selected cell; never changes the row selection
|
|
167
|
+
events: { clickMode: 'immediate' /* 'waitForDoubleClick' */, doubleClickDelay: 250, preventContextMenu: false },
|
|
168
|
+
tooltips: { enabled: true, cells: 'truncated', headers: 'truncated', showDelay: 500, hideDelay: 100, placement: 'top', disabledTypes: [] },
|
|
169
|
+
rowHeight: 36, // fixed row height (px)
|
|
170
|
+
headerHeight: 40,
|
|
171
|
+
summaryRowHeight: 36,
|
|
172
|
+
stripedRows: true,
|
|
173
|
+
resizableColumns: true,
|
|
174
|
+
keyboardNavigation: true,
|
|
175
|
+
locale: undefined, // browser locale for numbers, dates and text sorting
|
|
176
|
+
theme: 'light', // 'dark' or your own name
|
|
177
|
+
ariaLabel: 'Data grid',
|
|
178
|
+
rowClass: (row, i) => (row.overdue ? 'is-overdue' : null),
|
|
179
|
+
rowStyle: undefined,
|
|
180
|
+
|
|
181
|
+
virtualization: { rows: true, columns: true, rowBuffer: 6, columnBuffer: 2, syncScroll: true },
|
|
182
|
+
sorting: {
|
|
183
|
+
enabled: true, multiSort: true, multiSortKey: 'shift', // 'shift' | 'ctrl' | 'always'
|
|
184
|
+
showSortIcon: true, sortIconVisibility: 'hover', // 'always' | 'sorted' | 'hover'
|
|
185
|
+
showSortIndex: true, cycle: ['asc', 'desc', null], nulls: 'last', initialSort: [],
|
|
186
|
+
},
|
|
187
|
+
filtering: {
|
|
188
|
+
enabled: true, showFilterIcon: true, filterIconVisibility: 'always', // 'always' | 'hover'
|
|
189
|
+
valueList: true, conditions: true, maxConditions: 2 /* 1–5 */, caseSensitive: false,
|
|
190
|
+
showSortButtons: true, valueListLimit: 100_000, initialFilter: {},
|
|
191
|
+
mode: 'menu', // 'row' | 'both': a filter row under the header
|
|
192
|
+
rowDebounce: 300, rowHeight: undefined /* rowHeight */,
|
|
193
|
+
},
|
|
194
|
+
editing: {
|
|
195
|
+
enabled: false, mode: 'cell', trigger: 'dblclick', commitOnBlur: true, gridEditMode: false,
|
|
196
|
+
allowAdd: false, allowDelete: false, addPosition: 'top', newRow: undefined, rowValidator: undefined,
|
|
197
|
+
confirmDelete: true, actionColumn: false, // true | { edit, delete, add, width }
|
|
198
|
+
batch: false, undoLimit: 100, loadingWhileSaving: true, batchToolbar: true, saveHandler: undefined,
|
|
199
|
+
},
|
|
200
|
+
summary: { enabled: undefined /* auto */, sticky: true, scope: 'filtered', showLabels: true },
|
|
201
|
+
loading: { enabled: true, useCustomTemplate: true, blockGrid: true, message: 'Loading…' },
|
|
202
|
+
emptyState: { enabled: true, useCustomTemplate: true, showWhenFiltered: true, message: 'No data found' },
|
|
203
|
+
errorState: { enabled: true, useCustomTemplate: true, message: 'Something went wrong while loading the data.' },
|
|
204
|
+
templates: { displayByType: {}, editByType: {}, header: undefined, summary: undefined, loading: undefined, empty: undefined, error: undefined },
|
|
205
|
+
text: { apply: 'Apply' /* …every label, for translation */ },
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Templates
|
|
210
|
+
|
|
211
|
+
### Declarative templates (`ng-template` inside `<infi-grid>`)
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
<infi-grid [data]="rows" [columns]="columns">
|
|
215
|
+
<!-- one column -->
|
|
216
|
+
<ng-template infiGridCell="status" let-value let-row="row">
|
|
217
|
+
<span class="badge" [class.late]="row.late">{{ value }}</span>
|
|
218
|
+
</ng-template>
|
|
219
|
+
|
|
220
|
+
<!-- every column of a data type -->
|
|
221
|
+
<ng-template infiGridCell type="boolean" let-value>{{ value ? '✓' : '—' }}</ng-template>
|
|
222
|
+
|
|
223
|
+
<!-- edit mode -->
|
|
224
|
+
<ng-template infiGridEdit="notes" let-value let-setValue="setValue" let-commit="commit" let-cancel="cancel">
|
|
225
|
+
<input #box [value]="value ?? ''" (input)="setValue(box.value)" (keydown.enter)="commit()" (keydown.escape)="cancel()" />
|
|
226
|
+
</ng-template>
|
|
227
|
+
|
|
228
|
+
<ng-template infiGridHeader="amount" let-column>💰 {{ column.header }}</ng-template>
|
|
229
|
+
<ng-template infiGridSummary="amount" let-formatted="formattedValue">Σ {{ formatted }}</ng-template>
|
|
230
|
+
<ng-template infiGridLoading>Fetching…</ng-template>
|
|
231
|
+
<ng-template infiGridEmpty let-reason let-clear="clearFilters">
|
|
232
|
+
@if (reason === 'noData') { No orders yet } @else { No match. <button (click)="clear()">Clear filters</button> }
|
|
233
|
+
</ng-template>
|
|
234
|
+
<ng-template infiGridError let-message="message">Could not load: {{ message }}</ng-template>
|
|
235
|
+
</infi-grid>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Templates in configuration (`TemplateRef` or component)
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
columns = [
|
|
242
|
+
{ field: 'progress', type: 'number', displayTemplate: this.progressTpl }, // @ViewChild TemplateRef
|
|
243
|
+
{ field: 'status', displayTemplate: StatusBadgeComponent }, // component class
|
|
244
|
+
{ field: 'rating', type: 'number', editTemplate: RatingEditorComponent },
|
|
245
|
+
];
|
|
246
|
+
config = { templates: { displayByType: { date: MyDateCell }, error: ErrorPanelComponent } };
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
One template for many kinds of columns: set `templateKey` to the kind, so scrolling sideways reuses each cell for a column of the same kind instead of re-rendering its content (no blank columns while scrolling fast, even with rich cells):
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<ng-template infiGridCell let-value let-column="column">
|
|
253
|
+
@switch (column.config.meta.kind) {
|
|
254
|
+
@case ('switch') { <app-switch-cell [value]="value" /> }
|
|
255
|
+
@case ('color') { <app-color-cell [value]="value" /> }
|
|
256
|
+
@default { {{ value }} }
|
|
257
|
+
}
|
|
258
|
+
</ng-template>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
columns = fields.map((f) => ({ field: f.name, meta: { kind: f.kind }, templateKey: f.kind }));
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A component used as a template receives its context through an input named `context`. Extend `GridTemplateComponent` to get it declared:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
@Component({ template: `<b>{{ context().value }}</b>`, changeDetection: ChangeDetectionStrategy.OnPush })
|
|
269
|
+
export class StatusBadgeComponent extends GridTemplateComponent<GridCellContext<Order, string>> {}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Which template is used
|
|
273
|
+
|
|
274
|
+
The most specific wins: column property → field directive → type template (`config.templates.*ByType`, then `type` directive) → `defaultColumn` → directive without field/type → built-in.
|
|
275
|
+
|
|
276
|
+
### Template context
|
|
277
|
+
|
|
278
|
+
| Template | Context (`let-x="name"`) |
|
|
279
|
+
|---|---|
|
|
280
|
+
| Cell (display) | `$implicit`/`value`, `formattedValue`, `row`, `rowIndex`, `dataIndex`, `column`, `columnIndex`, `field`, `editing`, `rowEditing`, `api` |
|
|
281
|
+
| Edit | everything above plus `originalValue`, `error`, `editorOptions`, `autoFocus`, `setValue(v)`, `commit()`, `cancel()` |
|
|
282
|
+
| Header | `$implicit`/`column`, `columnIndex`, `header`, `sortDirection`, `sortIndex`, `filterActive`, `api`, `toggleSort(multi?)`, `openFilter()` |
|
|
283
|
+
| Summary | `$implicit`/`value`, `formattedValue`, `label`, `result`, `column`, `field`, `api` |
|
|
284
|
+
| Loading | `$implicit`/`loading`, `message`, `api` |
|
|
285
|
+
| Empty | `$implicit`/`reason` (`'noData'` or `'noResults'`), `message`, `api`, `clearFilters()` |
|
|
286
|
+
| Error | `$implicit`/`error`, `message`, `api`, `dismiss()` |
|
|
287
|
+
|
|
288
|
+
## Editing
|
|
289
|
+
|
|
290
|
+
- Turn editing on for every column (`editing.enabled: true`) or per column (`editable: true`, or a function of the row).
|
|
291
|
+
- Start editing with a double-click (or `trigger: 'click'`), Enter or F2 on the focused cell, or `api.enterEditMode(rowIndex, field?)`.
|
|
292
|
+
- Enter saves, Escape cancels, and Tab moves to the next editable cell. Clicking away saves (`commitOnBlur`).
|
|
293
|
+
- **Modes:**
|
|
294
|
+
- `mode: 'cell'` (default): one cell at a time.
|
|
295
|
+
- `mode: 'row'`: every editable cell of the row, saved together.
|
|
296
|
+
- **Grid edit mode:** `api.setGridEditMode(true)` or `editing.gridEditMode`. Every editable cell shows its editor, and each change saves immediately. This is the toggle between display mode and edit mode for the whole grid.
|
|
297
|
+
- **Built-in editors:** `text`, `number`, `select` (when `editorOptions.options` is set), `checkbox`, `date`, chosen from the column type or `editor`.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
{ field: 'status', editorOptions: { options: [{ label: 'Open', value: 'open' }, { label: 'Closed', value: 'closed' }] } }
|
|
301
|
+
{ field: 'qty', type: 'number', editorOptions: { min: 1, step: 1 }, validator: (v) => (v >= 1 ? null : 'At least 1') }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- A failing `validator` keeps the cell in edit mode and shows the message.
|
|
305
|
+
- `(cellValueChange)` fires for every saved value; `(editStart)`, `(editCommit)` and `(editCancel)` fire for the session.
|
|
306
|
+
- Saving an edit re-renders only that row and recalculates only the affected summaries. Rows don't jump: sorting and filtering are not re-applied until `api.refresh()`.
|
|
307
|
+
- Saving an edit goes through the same update as `api.updateRow()`: `required`, validators and `editing.rowValidator` run, `(rowUpdating)` can cancel it (the editor stays open), and `(rowUpdated)` reports it.
|
|
308
|
+
|
|
309
|
+
## Row identity and selection
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
config = {
|
|
313
|
+
primaryKey: 'id', // or 'customer.id', or (row) => `${row.region}-${row.no}`
|
|
314
|
+
selection: { mode: 'multiple' }, // checkbox column on by default in 'multiple' mode
|
|
315
|
+
};
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- **Identity:** with a `primaryKey`, rows are identified by key, not by position or object. Selection, edits and refreshes survive sorting, filtering, virtualization and new row objects with the same keys. Duplicate or missing keys are reported once through `(gridError)` (source `'data'`).
|
|
319
|
+
- **Selecting:** the mode decides what a click (on the checkbox, or on a cell or row with `selectOnClick`) and Space do.
|
|
320
|
+
- `'multiple'`: toggles that row and keeps the other selected rows. Shift+click selects the rows from the last clicked row to this one, in the current view order, keeping the rows selected before. Ctrl/Cmd+click is the same as a click.
|
|
321
|
+
- `'single'`: selects that row instead of the selected one; on the selected row, deselects it.
|
|
322
|
+
- Checkboxes always show the selection, right after any change. The header checkbox selects all rows in `selectAllScope` (the filtered rows by default) and shows an indeterminate state.
|
|
323
|
+
- Keys: Space toggles the focused row, Shift+↑/↓ extends the range, Ctrl/Cmd+A selects all.
|
|
324
|
+
- `rowSelectable: (row) => boolean` disables rows.
|
|
325
|
+
- With `selectOnClick: false`, or while [cell selection](#cell-selection) is on, only the checkboxes, the keyboard and the API select rows.
|
|
326
|
+
- **Filters:** by default, rows hidden by a new filter are deselected (`onFilter: 'deselectHidden'`). Removing the filter doesn't select them again.
|
|
327
|
+
- **API** (on `api` and on the component):
|
|
328
|
+
- `selectRowByKey(key, { scroll })`, `deselectRowByKey`, `selectRowsByKeys`, `deselectRowsByKeys`, `toggleRowSelection`
|
|
329
|
+
- `selectAll({ scope })`, `deselectAll`, `clearSelection`
|
|
330
|
+
- `getSelectedRows`, `getSelectedKeys`, `isRowSelected`
|
|
331
|
+
- `getRowKey`, `getRowByKey`, `getRowIndexByKey`, `scrollToRowByKey`
|
|
332
|
+
- **Events:**
|
|
333
|
+
- `(selectionChange)`: once per operation, with `source`, `action`, `added`/`removed` (and their keys), `oldRow`/`newRow` in single mode, and the previous and current selection.
|
|
334
|
+
- `(rowSelected)`, `(rowDeselected)`, `(selectionCleared)`.
|
|
335
|
+
- `(selectionClick)`: cancelable, before a click or key changes the selection.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
@ViewChild(InfiGridComponent) grid!: InfiGridComponent<Order>;
|
|
339
|
+
selectFirstOverdue() {
|
|
340
|
+
const order = this.orders().find((o) => o.overdue);
|
|
341
|
+
if (order) this.grid.selectRowByKey(order.id, { scroll: true });
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Cell selection
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
config = {
|
|
349
|
+
primaryKey: 'id',
|
|
350
|
+
selection: { mode: 'none' }, // or rows too, with their checkboxes
|
|
351
|
+
cellSelection: { enabled: true, activeRow: true },
|
|
352
|
+
};
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **Selecting:** a click on a cell selects that cell, and the arrow keys, Home/End, Page Up/Down and Ctrl+Home/End move it (the first key selects the top-left visible cell). The selected cell has the focus outline, the class `is-selected` and `aria-selected="true"`.
|
|
356
|
+
- **Independent of row selection:** a cell click never selects or deselects rows and never emits `(selectionChange)`; `selection.selectOnClick` is ignored. Rows are still selected with the checkbox column, Space, Shift+↑/↓, Ctrl/Cmd+A and the API. No `selection` option affects cell selection.
|
|
357
|
+
- **Identity:** the selected cell is kept by row key and field, so it follows its row through sorting, filtering, scrolling and refreshes. A filter that hides the row hides the cell until the filter is removed. The cell is cleared when its row leaves the data (refresh, delete, a saved batch delete) or its column is removed.
|
|
358
|
+
- **Active row:** `activeRow: true` marks the selected cell's row with `.infi-grid__row--active` (`--infi-grid-active-row-bg`). It is visual only: it isn't in `getSelectedKeys()`, emits no `(selectionChange)` and has no checkbox. A selected row keeps the selected color.
|
|
359
|
+
- **Events:**
|
|
360
|
+
- `(cellClick)`, `(cellDoubleClick)`, `(rowClick)` fire as before. Their `cancel` stops the cell selection (and starting an edit).
|
|
361
|
+
- `(cellSelectionChange)`: `source` (`'click'`, `'keyboard'`, `'api'`, `'dataRefresh'`, `'delete'`, `'columns'`), `row`, `key`, `rowIndex`, `dataIndex`, `field`, `column`, `value`, `formattedValue`, `previous` (`{ key, field }`) and `event`. When the cell is cleared, `row`, `key`, `field` and `column` are null.
|
|
362
|
+
- **API:** `getSelectedCell()` (`{ row, key, field, rowIndex }` or null), `selectCell(key, field, { scroll })` (false when the row or column isn't displayed, or the cell is already selected), `clearCellSelection()`.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
onCellSelectionChange(e: GridCellSelectionChangeEvent<Order>) {
|
|
366
|
+
this.details.set(e.row); // null when the cell was cleared
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Add, update and delete
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
config = {
|
|
374
|
+
primaryKey: 'id',
|
|
375
|
+
editing: {
|
|
376
|
+
enabled: true,
|
|
377
|
+
allowAdd: true, allowDelete: true, // the UI: + and 🗑 buttons, the Delete key
|
|
378
|
+
actionColumn: true, // ✎ 🗑 per row (✓ ✕ while editing), + in the header
|
|
379
|
+
newRow: () => ({ id: nextId++ }), // initial object of new rows
|
|
380
|
+
rowValidator: (o) => (o.shipDate < o.orderDate ? { shipDate: 'Before the order date' } : null),
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
columns = [
|
|
384
|
+
{ field: 'customer', required: true },
|
|
385
|
+
{ field: 'status', defaultValue: 'open' },
|
|
386
|
+
{ field: 'quantity', type: 'number', defaultValue: 1, validator: (q) => (q > 0 ? null : 'At least 1') },
|
|
387
|
+
];
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
```html
|
|
391
|
+
<infi-grid [(data)]="orders" [columns]="columns" [config]="config" (rowAdded)="…" (rowUpdated)="…" (rowDeleted)="…" />
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- **API:**
|
|
395
|
+
- `addRow(row, { position: 'top' | 'bottom' | index, scroll })` and `beginAddRow({ position, values })` (the inline add row)
|
|
396
|
+
- `updateRow(key, { field: value, 'nested.path': value })`
|
|
397
|
+
- `deleteRow(key)` and `deleteRows(keys)` (no confirmation); `requestDeleteRows(keys)` and `deleteSelectedRows()` (with the confirmation)
|
|
398
|
+
- Each returns `{ ok, reason, errors, row, key }`. Reasons: `'cancelled'`, `'invalid'`, `'duplicateKey'`, `'missingKey'`, `'notFound'`, `'readOnly'`, `'primaryKeyChange'`, `'busy'`.
|
|
399
|
+
- **New rows:**
|
|
400
|
+
- The object you pass becomes the row. Column `defaultValue`s fill the missing fields.
|
|
401
|
+
- Then `required`, the column validators, `rowValidator` and the key (present and unique) are checked.
|
|
402
|
+
- A new row stays visible even if the active filter wouldn't show it, until the filter changes.
|
|
403
|
+
- **Updates** write into the row object. Its identity, selection and scroll position don't change, and keys can't change.
|
|
404
|
+
- **Deletes** remove the rows from the data, the selection and the summaries. An edit of a deleted row is cancelled.
|
|
405
|
+
- **Your array is never changed:** adds and deletes emit a new array through `(dataChange)`, so `[(data)]="orders"` keeps your signal in sync.
|
|
406
|
+
- **The inline add row** shows under the header (or above the summary row) with editors. Enter or ✓ adds it, Escape or ✕ discards it. Columns get an editor by `editableOnAdd`, which defaults to `editable`; the key column only gets one when `newRow` gives no key.
|
|
407
|
+
- **Delete confirmation:**
|
|
408
|
+
- A built-in dialog: "Delete 3 rows?". Escape cancels; Tab stays in the dialog.
|
|
409
|
+
- Or your own template: `<ng-template infiGridDeleteConfirm let-count="count" let-confirm="confirm" let-cancel="cancel">`.
|
|
410
|
+
- `confirmDelete: false` turns it off.
|
|
411
|
+
- **Events:** `(rowAdding)`, `(rowUpdating)` and `(rowDeleting)` are cancelable (`event.cancel = true`). `(rowAdded)`, `(rowUpdated)` (with `oldRow`, `oldValues`, `newValues` and `changedFields`) and `(rowDeleted)` follow.
|
|
412
|
+
|
|
413
|
+
## Batch editing
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
config = {
|
|
417
|
+
primaryKey: 'id',
|
|
418
|
+
editing: {
|
|
419
|
+
enabled: true, batch: true, allowAdd: true, allowDelete: true, actionColumn: true,
|
|
420
|
+
saveHandler: (changes) => this.http.post('/api/orders/batch', changes), // Promise or Observable
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
- **Changes stay pending.** Adds, updates and deletes are kept as pending changes, and your data isn't touched until saved.
|
|
426
|
+
- New rows are shown first (or last), modified rows and cells are marked, and deleted rows stay visible, struck through, with a restore button.
|
|
427
|
+
- Sorting, filtering and summaries use the pending values.
|
|
428
|
+
- **The toolbar** under the rows shows "3 unsaved changes" with Undo, Redo, Discard and Save. Turn it off with `batchToolbar: false`.
|
|
429
|
+
- Undo and redo also work with Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z (or Ctrl+Y).
|
|
430
|
+
- **`saveChanges(handler?)`:**
|
|
431
|
+
1. Commits an open edit.
|
|
432
|
+
2. Calls the handler (`editing.saveHandler` by default), showing the loading state meanwhile.
|
|
433
|
+
3. Applies everything to the data in one step: `(changesSaved)`, then `(dataChange)`.
|
|
434
|
+
- If the handler throws or rejects, nothing is applied and the changes stay pending (`(changesSaveFailed)`).
|
|
435
|
+
- The promise always resolves with `{ ok, reason, error, changes }`; it never rejects.
|
|
436
|
+
- **Other methods and events:**
|
|
437
|
+
- `cancelChanges()` discards everything (`(changesCancelled)`).
|
|
438
|
+
- `getPendingChanges()` returns `{ added, updated: [{ key, row, original, changes, oldValues }], deleted, … }`, and `hasPendingChanges()` tells whether there are any.
|
|
439
|
+
- `undo()`, `redo()`, `canUndo()` and `canRedo()`; `(pendingChange)` reports every change, for your own buttons.
|
|
440
|
+
- `(rowAdded)`, `(rowUpdated)` and `(rowDeleted)` fire with `pending: true` when a change is recorded. `(cellValueChange)` doesn't fire for pending changes.
|
|
441
|
+
|
|
442
|
+
## Filtering
|
|
443
|
+
|
|
444
|
+
- The filter icon in each header opens an Excel-style popup:
|
|
445
|
+
- "Clear filter" for the column and "Clear all filters";
|
|
446
|
+
- sort buttons;
|
|
447
|
+
- up to `maxConditions` (1–5) conditions with one AND/OR choice;
|
|
448
|
+
- a searchable, virtualized checkbox list of values, with counts.
|
|
449
|
+
Cancel and Escape close it without applying anything.
|
|
450
|
+
- **Filter row** (`filtering.mode: 'row'` or `'both'`): an input under each header, with an operator button and a clear button. Booleans get a select.
|
|
451
|
+
- It's shown while at least one displayed column is filterable (`filtering.enabled`, or the column's `filterable`).
|
|
452
|
+
- Typing applies after `rowDebounce` (300 ms); Enter applies at once, and Escape reverts.
|
|
453
|
+
- It edits the first condition of the column's filter, so the popup and the row show the same filter.
|
|
454
|
+
- Filtered columns show a filled funnel and an accent line under the header.
|
|
455
|
+
- **Operators by type:**
|
|
456
|
+
- **text:** contains, does not contain, equals, does not equal, begins with, ends with, is empty, is not empty
|
|
457
|
+
- **number:** =, ≠, >, ≥, <, ≤, between, empty, not empty
|
|
458
|
+
- **date:** equals, not equals, before, after, on or before, on or after, between, empty, not empty
|
|
459
|
+
- **boolean:** is true, is false, empty, not empty
|
|
460
|
+
- Through the API:
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
api.applyFilter('country', { values: ['Germany', 'France'] }); // checkbox list
|
|
464
|
+
api.applyFilter('amount', { operator: 'greaterThan', value: 1000 }); // one condition
|
|
465
|
+
api.applyFilter('date', { conditions: [{ operator: 'onOrAfter', value: '2025-01-01' }, { operator: 'before', value: '2025-02-01' }], logic: 'and' });
|
|
466
|
+
api.clearFilter('amount'); // or api.clearFilter() / api.clearAllFilters() for all
|
|
467
|
+
api.isFiltered('amount'); // api.isFiltered() for any column; api.getDisplayedRowCount()
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
- Custom operators: `filter: { customOperators: [{ key: 'even', label: 'Is even', inputs: 0, predicate: (v) => v % 2 === 0 }] }`.
|
|
471
|
+
- Fully custom logic: `filter: { predicate: (row, model, value) => … }`.
|
|
472
|
+
|
|
473
|
+
## Sorting
|
|
474
|
+
|
|
475
|
+
- Click a header to cycle asc → desc → none. Shift+click (configurable) adds a column to a multi-column sort; the order shows as 1, 2, 3 next to the icon.
|
|
476
|
+
- API: `api.applySort('amount', 'desc')`, `api.applySort([{ field: 'country', direction: 'asc' }, { field: 'amount', direction: 'desc' }])`, `api.clearSort()`.
|
|
477
|
+
- Text sorts case-insensitively and number-aware ("Item 2" before "Item 10"). Blanks go last. Provide `sort.comparator` for custom ordering.
|
|
478
|
+
|
|
479
|
+
## Summaries
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
{ field: 'amount', type: 'number', summary: 'sum' }
|
|
483
|
+
{ field: 'price', type: 'number', summary: { type: 'avg', label: 'Average' } }
|
|
484
|
+
{ field: 'margin', type: 'number', summary: {
|
|
485
|
+
label: 'Weighted',
|
|
486
|
+
// custom formula: gets data, rows, values, column, field, api, scope, filter/sort model, getSummary(), fn helpers
|
|
487
|
+
fn: (ctx) => ctx.fn.sum(ctx.rows.map((r) => r.amount * r.margin)) / (ctx.getSummary('amount') as number),
|
|
488
|
+
dependsOn: ['amount', 'margin'], // recalculate only when these fields are edited
|
|
489
|
+
} }
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
- `summary.sticky: true` pins the summary row to the bottom (like a pinned row); `false` shows it as a normal last row.
|
|
493
|
+
- `summary.scope: 'filtered' | 'all'` (global or per column).
|
|
494
|
+
- `api.recalculateSummary()` and `api.getSummaryValues()`.
|
|
495
|
+
|
|
496
|
+
## Tooltips
|
|
497
|
+
|
|
498
|
+
On by default for text that is cut off (ellipsis): the tooltip shows the full text after `showDelay`.
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
config = { tooltips: { cells: 'truncated' /* 'always' | 'never' */, headers: 'truncated', showDelay: 500, placement: 'top', disabledTypes: ['boolean'] } };
|
|
502
|
+
columns = [
|
|
503
|
+
{ field: 'notes', tooltip: 'always' },
|
|
504
|
+
{ field: 'total', tooltip: (ctx) => `${ctx.row.quantity} × ${ctx.row.unitPrice}` },
|
|
505
|
+
{ field: 'margin', description: 'Gross margin after discounts' }, // always shown in the header tooltip
|
|
506
|
+
{ field: 'id', tooltip: false, headerTooltip: false },
|
|
507
|
+
];
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```html
|
|
511
|
+
<ng-template infiGridCellTooltip="customer" let-row="row">{{ row.customer.name }}, {{ row.customer.country }}</ng-template>
|
|
512
|
+
<ng-template infiGridHeaderTooltip="margin" let-column let-description="description"><b>{{ column.header }}</b>: {{ description }}</ng-template>
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- **One tooltip per grid**, in `<body>` so it is never clipped. It is removed with the grid.
|
|
516
|
+
- It hides on scroll, click, key press and any re-render, so recycled rows never keep a stale tooltip.
|
|
517
|
+
- It flips to the other side near the window edges.
|
|
518
|
+
- **Keyboard:** arrow-key focus on a cut-off cell shows its tooltip (focusing a header's filter button shows the header's). Escape hides it.
|
|
519
|
+
- **Accessibility:** it has `role="tooltip"` and sets `aria-describedby` on its cell.
|
|
520
|
+
|
|
521
|
+
## Loading, empty and error states
|
|
522
|
+
|
|
523
|
+
```html
|
|
524
|
+
<infi-grid [data]="rows()" [columns]="columns" [loading]="loading()" [error]="loadError()" />
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- **Loading:** an overlay over the grid only; the rest of your app stays usable. `loading.blockGrid` decides whether the grid itself can be used meanwhile.
|
|
528
|
+
- **Empty:** shown when the data array is empty, and also when filters remove every row (`showWhenFiltered`). `useCustomTemplate: false` switches back to the built-in message.
|
|
529
|
+
- **Error:** shown when `[error]` (or `api.setError()`) is set. It's independent of the empty state and takes precedence. `errorState.enabled: false` hides it.
|
|
530
|
+
|
|
531
|
+
## Data refresh
|
|
532
|
+
|
|
533
|
+
| Call | What it does |
|
|
534
|
+
|---|---|
|
|
535
|
+
| a new `[data]` array, `setData(rows)`, `updateData(rows)` | Replace the data: re-sort, re-filter, summaries, and reconcile selection and edits by key |
|
|
536
|
+
| `refreshData()`, `refresh()` | Re-read the same array after `push`/`splice` or changes to its objects |
|
|
537
|
+
| `refreshRows(rows)` | Re-render only these rows after changing them in place |
|
|
538
|
+
| `updateCell(rowIndex, field, value)` / `updateRow(key, changes)` | Change values (the row re-renders, rows don't move) |
|
|
539
|
+
|
|
540
|
+
`config.dataRefresh` (or the options of `setData`/`refreshData`) decides what a refresh keeps: `filter`, `sort` and `selection` (`'keep'` or `'reset'`), and `scroll` (`'keep'` or `'top'`). `(dataRefresh)` reports `{ reason, rowCount, displayedRowCount, removedSelectionKeys, filterReset, sortReset }`; `(loadingChange)` reports each change of the loading state.
|
|
541
|
+
|
|
542
|
+
## Grid API
|
|
543
|
+
|
|
544
|
+
Get the API from `(gridReady)="api = $event.api"`, from the component's `api`, or call the methods on the component itself (`@ViewChild(InfiGridComponent) grid; grid.selectRowByKey(1)`).
|
|
545
|
+
|
|
546
|
+
| Area | Methods |
|
|
547
|
+
|---|---|
|
|
548
|
+
| State | `getConfig`, `getColumns`, `getColumn`, `getDisplayedColumns`, `getVisibleColumns`, `getColumnState`, `getColumnWidth`, `getData`, `getDisplayedRows`, `getVisibleRows`, `getRow`, `getFilterModel`, `getSortModel`, `getSummaryConfig`, `getSummaryValues`, `getVirtualizationState`, `getEditState` |
|
|
549
|
+
| Rows by key | `getRowKey`, `getRowByKey`, `getRowIndexByKey`, `scrollToRowByKey` |
|
|
550
|
+
| Filtering | `applyFilter`, `setFilterModel`, `clearFilter`, `clearAllFilters`, `isFiltered`, `getDisplayedRowCount`, `openFilter`, `closeFilter` |
|
|
551
|
+
| Sorting | `applySort`, `clearSort` |
|
|
552
|
+
| Data | `setData`, `updateData`, `refreshData`, `refresh`, `updateCell`, `refreshRows` |
|
|
553
|
+
| Editing | `enterEditMode`, `exitEditMode`, `isEditing`, `setGridEditMode`, `isGridEditMode` |
|
|
554
|
+
| Selection | `selectRowByKey`, `deselectRowByKey`, `selectRowsByKeys`, `deselectRowsByKeys`, `toggleRowSelection`, `selectAll`, `deselectAll`, `clearSelection`, `getSelectedRows`, `getSelectedKeys`, `isRowSelected` |
|
|
555
|
+
| Cell selection | `getSelectedCell`, `selectCell`, `clearCellSelection` |
|
|
556
|
+
| Add, update, delete | `addRow`, `beginAddRow`, `updateRow`, `deleteRow`, `deleteRows`, `requestDeleteRows`, `deleteSelectedRows` |
|
|
557
|
+
| Batch | `getPendingChanges`, `hasPendingChanges`, `saveChanges`, `cancelChanges`, `undo`, `redo`, `canUndo`, `canRedo` |
|
|
558
|
+
| Summaries | `recalculateSummary` |
|
|
559
|
+
| Scrolling | `scrollToRow(index, 'auto' \| 'start' \| 'center' \| 'end')`, `scrollToColumn(field \| index, …)` |
|
|
560
|
+
| Columns | `setColumns`, `setColumnWidth`, `resetColumnWidth`, `setColumnVisible` |
|
|
561
|
+
| States | `setLoading`, `isLoading`, `setError`, `getError` |
|
|
562
|
+
|
|
563
|
+
Row indexes refer to the current (sorted and filtered) view. `getVisibleRows()` and `getVisibleColumns()` return what's in the viewport right now; `getDisplayedRows()` returns every row that passes the filters, in display order.
|
|
564
|
+
|
|
565
|
+
## Events
|
|
566
|
+
|
|
567
|
+
| Area | Outputs |
|
|
568
|
+
|---|---|
|
|
569
|
+
| Grid and data | `gridReady`, `dataRefresh`, `loadingChange`, `dataChange`, `viewportChange`, `gridError` |
|
|
570
|
+
| Sorting, filtering, columns | `sortChange`, `filterChange`, `columnResize` (`oldWidth`, `newWidth`, `finished`, `source`) |
|
|
571
|
+
| Clicks and pointer | `cellClick`, `cellDoubleClick`, `rowClick`, `rowDoubleClick`, `headerClick`, `cellContextMenu`, `cellMouseEnter`, `cellMouseLeave` |
|
|
572
|
+
| Selection | `selectionChange`, `rowSelected`, `rowDeselected`, `selectionCleared`, `selectionClick`, `cellSelectionChange` |
|
|
573
|
+
| Editing | `cellValueChange`, `editStart`, `editCommit`, `editCancel` |
|
|
574
|
+
| Add, update, delete | `rowAdding`, `rowAdded`, `rowUpdating`, `rowUpdated`, `rowDeleting`, `rowDeleted` |
|
|
575
|
+
| Batch | `changesSaved`, `changesCancelled`, `changesSaveFailed`, `pendingChange` |
|
|
576
|
+
|
|
577
|
+
- **Context:** cell events carry `row`, `key`, `rowIndex`, `dataIndex`, `field`, `column`, `value` and `formattedValue`, so you never need to read the DOM.
|
|
578
|
+
- **Cancelling:** set `event.cancel = true` in `cellClick`, `rowClick`, `cellDoubleClick`, `rowDoubleClick`, `headerClick` or `selectionClick` to stop the grid's own reaction (row or cell selection, starting an edit, sorting).
|
|
579
|
+
- **Double clicks:** `events.clickMode: 'waitForDoubleClick'` holds clicks back for `doubleClickDelay`, so a double click emits only `cellDoubleClick`.
|
|
580
|
+
- **No duplicates:** events come from the grid's state, not from DOM elements, so recycled rows never duplicate them. Events caused by an input change are emitted right after that change detection.
|
|
581
|
+
|
|
582
|
+
The grid never writes to the console. Problems it catches (for example a custom summary function that throws, which shows `#ERROR`) are reported through `(gridError)`.
|
|
583
|
+
|
|
584
|
+
## Styling and theming
|
|
585
|
+
|
|
586
|
+
- **Per cell or column:** `cellClass` / `cellStyle` (static or functions of the cell), `headerClass` / `headerStyle`, and `rowClass` / `rowStyle` in the config.
|
|
587
|
+
- **Themes:** set CSS variables on the grid, a wrapper or `:root`:
|
|
588
|
+
|
|
589
|
+
```css
|
|
590
|
+
infi-grid.orders {
|
|
591
|
+
--infi-grid-accent: #7c3aed;
|
|
592
|
+
--infi-grid-header-bg: #f5f3ff;
|
|
593
|
+
--infi-grid-font-size: 14px;
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Variables: `--infi-grid-font-family`, `-font-size`, `-bg`, `-fg`, `-muted-fg`, `-border-color`, `-cell-border-color`, `-cell-padding`, `-header-bg`, `-header-fg`, `-row-alt-bg`, `-row-hover-bg`, `-accent`, `-accent-fg`, `-summary-bg`, `-editing-bg`, `-invalid`, `-popup-bg`, `-popup-shadow`, `-input-bg`, `-radius`, `-overlay-bg`, `-selected-bg`, `-selected-hover-bg`, `-active-row-bg`, `-selected-cell-bg`, `-danger`, `-added-color`, `-modified-color`, `-modified-bg`, `-tooltip-bg`, `-tooltip-fg`.
|
|
598
|
+
- **Dark theme:** `config.theme = 'dark'`. A custom theme `'x'` adds the class `infi-grid-theme-x` for your own variables.
|
|
599
|
+
- **Useful classes:**
|
|
600
|
+
- rows: `.infi-grid__row--odd`, `--editing`, `--selected`, `--active` (cell selection), and in batch mode `--added`, `--modified`, `--deleted`
|
|
601
|
+
- cells: `.infi-grid__cell.is-editing`, `.is-focused`, `.is-selected` (cell selection), `.is-invalid`, `.is-modified`
|
|
602
|
+
- headers: `.infi-grid__header-cell.is-sorted`, `.is-filtered`
|
|
603
|
+
- others: `.infi-grid__summary--sticky`, `.infi-grid-tooltip`
|
|
604
|
+
|
|
605
|
+
## Keyboard
|
|
606
|
+
|
|
607
|
+
| Keys | Action |
|
|
608
|
+
|---|---|
|
|
609
|
+
| Arrow keys, Page Up/Down, Home/End (Ctrl+Home/End) | Move the focused cell, which is the selected cell with `cellSelection` (the grid scrolls to it) |
|
|
610
|
+
| Enter or F2 | Edit the focused cell |
|
|
611
|
+
| Enter (in an editor) | Save |
|
|
612
|
+
| Escape | Cancel editing, close the filter popup, or hide a tooltip |
|
|
613
|
+
| Tab / Shift+Tab (in an editor) | Save and edit the next / previous editable cell |
|
|
614
|
+
| Space, Shift+↑/↓, Ctrl/Cmd+A | Toggle, extend or select all rows (selection) |
|
|
615
|
+
| Delete | Delete the selected rows, or the focused row (with `allowDelete`) |
|
|
616
|
+
| Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z or Ctrl+Y | Undo and redo (batch mode) |
|
|
617
|
+
| Tab / Shift+Tab (in the filter row) | The next or previous column's filter |
|
|
618
|
+
|
|
619
|
+
## Performance tips
|
|
620
|
+
|
|
621
|
+
- Scrolling runs outside Angular's zone and checks only the grid, so your app's change detection never runs while the user scrolls.
|
|
622
|
+
- Fast scrolling never shows blank rows or columns (`virtualization.syncScroll`, on by default). Browsers scroll on their own thread, ahead of the page's code; the grid keeps the drawn content in place while they do, and moves it in the frame it draws the rows and columns for the new position. On a slow PC the content can follow the scrollbar a frame late, but it is never blank.
|
|
623
|
+
- Row and cell elements are reused, never recreated. Template views are reused too.
|
|
624
|
+
- Prefer `ng-template` or `OnPush` components in cell templates, and keep them light (no heavy pipes or functions).
|
|
625
|
+
- Replace the data array (`data = [...data]`) or call `api.refresh()` after mutating it in place.
|
|
626
|
+
- Only listen to `(viewportChange)` if you need it; it re-enters Angular's zone on every scroll frame.
|
|
627
|
+
|
|
628
|
+
## Notes and limitations
|
|
629
|
+
|
|
630
|
+
- All rows share one fixed height (`rowHeight`). That's what makes 1M-row virtualization O(1).
|
|
631
|
+
- Above about 416,000 rows at 36px, the grid switches to scaled scrolling, because browsers cap element height.
|
|
632
|
+
- Rows and cells are positioned absolutely and reused, so their DOM order can differ from the visual order. ARIA row and column indexes give the logical order.
|
|
633
|
+
- After editing, rows stay where they are; call `api.refresh()` to re-sort and re-filter.
|
|
634
|
+
- Keys are immutable: change a row's key by deleting it and adding a new row.
|
|
635
|
+
|
|
636
|
+
## Coming from angpackage-grid
|
|
637
|
+
|
|
638
|
+
InfiGrid 1.0.0 is angpackage-grid 1.2.1 under its new name. Inputs, outputs, configuration, the grid API, events and types are unchanged; only the names below change:
|
|
639
|
+
|
|
640
|
+
| angpackage-grid | InfiGrid |
|
|
641
|
+
|---|---|
|
|
642
|
+
| `npm install angpackage-grid` | `npm install infi-grid` |
|
|
643
|
+
| `import { … } from 'angpackage-grid'` | `import { … } from 'infi-grid'` |
|
|
644
|
+
| `<ang-grid>` | `<infi-grid>` |
|
|
645
|
+
| `AngGridComponent`, `AngGridModule`, `ANG_GRID` | `InfiGridComponent`, `InfiGridModule`, `INFI_GRID` |
|
|
646
|
+
| Template directives `angGridCell`, `angGridEdit`, `angGridHeader`, `angGridSummary`, `angGridLoading`, `angGridEmpty`, `angGridError`, `angGridDeleteConfirm`, `angGridCellTooltip`, `angGridHeaderTooltip` | `infiGridCell`, `infiGridEdit`, `infiGridHeader`, `infiGridSummary`, `infiGridLoading`, `infiGridEmpty`, `infiGridError`, `infiGridDeleteConfirm`, `infiGridCellTooltip`, `infiGridHeaderTooltip` |
|
|
647
|
+
| CSS classes `.ang-grid`, `.ang-grid__*`, `.ang-grid-theme-*`, `.ang-grid-popup`, `.ang-grid-menu`, `.ang-grid-tooltip` | `.infi-grid`, `.infi-grid__*`, `.infi-grid-theme-*`, `.infi-grid-popup`, `.infi-grid-menu`, `.infi-grid-tooltip` |
|
|
648
|
+
| Theme variables `--ang-grid-*` | `--infi-grid-*` |
|
|
649
|
+
|
|
650
|
+
A search and replace of `ang-grid` → `infi-grid`, `angGrid` → `infiGrid`, `AngGrid` → `InfiGrid` and `ANG_GRID` → `INFI_GRID`, plus the package name in imports and `package.json`, moves an app over.
|
|
651
|
+
|
|
652
|
+
## Compatibility
|
|
653
|
+
|
|
654
|
+
Built with Angular 19 (partial compilation). It works in Angular 19 apps and newer, with or without zone.js. Styles are included in the component, so no `angular.json` changes are needed.
|