@thkl/agrid 0.1.5 → 0.1.9

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/README.md CHANGED
@@ -30,7 +30,12 @@ interface Person {
30
30
 
31
31
  const columns: ColDef<Person>[] = [
32
32
  { field: 'id', header: 'ID', editable: false },
33
- { field: 'name', header: 'Name', filterable: true },
33
+ {
34
+ field: 'name',
35
+ header: 'Name',
36
+ filterable: true,
37
+ infoIcon: ({ row }) => Boolean(row.name),
38
+ },
34
39
  ];
35
40
 
36
41
  @Component({
@@ -55,6 +60,10 @@ export class PeopleComponent {
55
60
  }
56
61
  ```
57
62
 
63
+ Set `infoIcon: true` to show a right-aligned `?` action for every cell in a
64
+ column, or use a predicate to enable it per row. Listen to `(cellInfo)` to
65
+ receive the row, field, value, original row index, and column definition.
66
+
58
67
  Set `group: 'employee'` on adjacent columns to render the `Employee` label above them. Dragging
59
68
  that grouped header moves its current contiguous column segment as one block. Reordering, hiding,
60
69
  or pinning may split one group ID into multiple headers. Segments containing locked columns cannot
@@ -71,11 +80,188 @@ Marking is independent from row selection. Cell and range copy use the same copi
71
80
  every marked row, while Copy row uses every visible column. Duplicate rows are omitted, and marked
72
81
  rows remain included when filters hide them.
73
82
 
83
+ ## Menu bar
84
+
85
+ Set `menuBarItems` to render command buttons above the headers. An item with `items` becomes a
86
+ split button: its label emits the parent id and its chevron opens additional commands. Every
87
+ command emits through the single `(menuBarAction)` output.
88
+
89
+ ```ts
90
+ readonly provider = new AgridProvider<Person>({
91
+ columns,
92
+ datasource,
93
+ menuBarItems: [
94
+ { id: 'refresh', label: 'Refresh', icon: '↻' },
95
+ {
96
+ id: 'selection',
97
+ label: 'Selection',
98
+ disabled: ({ selectedRows }) => selectedRows.length === 0,
99
+ items: [
100
+ { id: 'activate', label: 'Activate' },
101
+ { id: 'deactivate', label: 'Deactivate', active: ({ rows }) => rows.some(row => !row.active) },
102
+ ],
103
+ },
104
+ ],
105
+ });
106
+ ```
107
+
108
+ ```html
109
+ <agrid [provider]="provider" (menuBarAction)="runCommand($event)" />
110
+ ```
111
+
112
+ `visible`, `active`, and `disabled` may be booleans or callbacks. Callback context includes
113
+ `rows`, `selectedRows`, `selectedCell`, `provider`, and `datasource`.
114
+
115
+ ## Input masks
116
+
117
+ Use `inputMask` to select a string mask for each row and cell. The callback
118
+ receives `{ row, value, column }`; return `null` for cells that should remain
119
+ unrestricted.
120
+
121
+ ```ts
122
+ {
123
+ field: 'reference',
124
+ header: 'Reference',
125
+ inputMask: ({ row }) =>
126
+ row.numeric
127
+ ? /\d{0,3}(?:-\d{0,5}(?:-\d{0,5})?)?/
128
+ : /[a-z0-9]{0,3}(?: [a-z0-9]{0,3}(?: [a-z0-9]{0,5})?)?/i,
129
+ }
130
+ ```
131
+
132
+ The regex is matched against the complete proposed value and must allow
133
+ partial input. Separators are typed explicitly rather than inserted
134
+ automatically. Invalid edits revert to the last accepted value.
135
+
136
+ ## Boolean columns
137
+
138
+ Set `type: 'boolean'` to render a cell as an inline checkbox. Clicking it toggles the value and
139
+ commits immediately — no edit mode, and the change is recorded in undo/redo like any other edit.
140
+ Read-only columns (`editable: false`) or a read-only grid render the checkbox disabled. Values are
141
+ truthy-coerced for display, so `true`, `1`, `'true'`, and `'1'` all render as checked.
142
+
143
+ ```ts
144
+ const columns: ColDef<Person>[] = [
145
+ { field: 'name', header: 'Name' },
146
+ { field: 'active', header: 'Active', type: 'boolean' },
147
+ ];
148
+ ```
149
+
150
+ ## Runtime readonly cells
151
+
152
+ Use `cellReadonly` when editability depends on the current row. Returning `true` blocks inline
153
+ editing, boolean toggles, paste, fill, and sidebar edits for that cell.
154
+
155
+ ```ts
156
+ const columns: ColDef<Order>[] = [
157
+ { field: 'status', header: 'Status' },
158
+ {
159
+ field: 'approvedBy',
160
+ header: 'Approved by',
161
+ cellReadonly: ({ row }) => row.status !== 'Draft',
162
+ },
163
+ ];
164
+ ```
165
+
166
+ ## Condition filters
167
+
168
+ Mark a column `filterable: true` and its column menu gains a **condition** filter in addition to
169
+ the value picker. Text columns offer equals, not equal, like, starts with, ends with, includes, and
170
+ does not include. Numbers offer `=`, `≠`, `>`, `≥`, `<`, `≤`, and `between`; dates offer on /
171
+ before / after / between. Conditions combine with the header text filter, value picker, and other
172
+ columns using AND semantics, and are included in `AgridControl.toJSON()` state.
173
+
174
+ ```ts
175
+ const columns: ColDef<Order>[] = [
176
+ { field: 'reference', header: 'Reference', filterable: true },
177
+ { field: 'total', header: 'Total', type: 'number', filterable: true },
178
+ { field: 'placedAt', header: 'Placed', type: 'date', filterable: true },
179
+ ];
180
+ ```
181
+
182
+ Set it programmatically with `control.setRangeFilter(field, operator, operand, operand2?)`, where
183
+ `operator` can also be `'like' | 'startsWith' | 'endsWith' | 'includes' | 'notIncludes'` for text
184
+ columns (pass `null` to clear). `like` uses `%` for any sequence and `_` for one character.
185
+
186
+ ## Edit validation
187
+
188
+ Add a `validate` hook to a column to reject bad values. Return a message to block the edit (the
189
+ value is not written and the message is shown), or `null` to accept it. It runs on inline commit,
190
+ boolean-checkbox toggle, and sidebar save.
191
+
192
+ ```ts
193
+ const columns: ColDef<Person>[] = [
194
+ { field: 'email', header: 'Email',
195
+ validate: v => /@/.test(String(v)) ? null : 'Enter a valid email' },
196
+ { field: 'age', header: 'Age', type: 'number',
197
+ validate: (v, row) => Number(v) >= 0 ? null : 'Age must be ≥ 0' },
198
+ ];
199
+ ```
200
+
201
+ A rejected inline edit keeps the cell in edit mode with the message shown beneath it, so the user
202
+ can correct it; Tab/Enter won't leave the cell until the value is valid. Rejected sidebar edits show
203
+ the message under the field. Listen to `(validationFailed)` for `{ rowIndex, field, value, message,
204
+ source }` (`source` is `'inline'` or `'sidebar'`).
205
+
206
+ ```html
207
+ <agrid [provider]="provider" (validationFailed)="onInvalid($event)" />
208
+ ```
209
+
210
+ ## Quick filter
211
+
212
+ Set `enableQuickFilter: true` to render a search box above the grid that keeps rows whose visible
213
+ columns contain the text (resolved display values, so `ValueOption` labels and formatters count).
214
+
215
+ ```ts
216
+ readonly provider = new AgridProvider<Person>({ columns, datasource, enableQuickFilter: true });
217
+ ```
218
+
219
+ Drive it programmatically with `control.setQuickFilter(text)`; it's part of `toJSON()` state and is
220
+ cleared by `control.clearAllFilters()`.
221
+
222
+ ## Server-side filtering
223
+
224
+ With `serverSideFiltering: true` the grid never filters locally — it emits events so the host can
225
+ refetch:
226
+
227
+ - `(filterChange)` — header text filters emit `{ field, value }`; menu conditions emit
228
+ `{ field, value: '', operator, operand, operand2 }` (operator `null` clears the condition).
229
+ - `(sortChange)` — `{ field, direction }`.
230
+ - `(quickFilterChange)` — the quick-filter text (debounced by `filterDebounceMs`).
231
+
232
+ ```html
233
+ <agrid [provider]="provider"
234
+ (filterChange)="onFilter($event)"
235
+ (sortChange)="onSort($event)"
236
+ (quickFilterChange)="onQuickFilter($event)" />
237
+ ```
238
+
239
+ Text/range/quick events are debounced by `filterDebounceMs` (default 300 ms; `0` disables). The
240
+ Excel-style value picker stays client-only and is hidden in this mode.
241
+
242
+ ## Grouping and aggregates
243
+
244
+ Give a column an `aggregate` (`'sum'`, `'avg'`, `'min'`, `'max'`, `'count'`, or a custom
245
+ `(values) => unknown` function) and the grid renders a footer row with that column's total over all
246
+ filtered rows. Set/clear it at runtime with `control.setAggregate(field, fn)`.
247
+
248
+ ```ts
249
+ const columns: ColDef<Order>[] = [
250
+ { field: 'region', header: 'Region', groupable: true },
251
+ { field: 'total', header: 'Total', type: 'number', aggregate: 'sum' },
252
+ ];
253
+ ```
254
+
255
+ When the grid is grouped (set `groupable: true` and group from the column menu, or call
256
+ `control.setGroupBy(field)`), each **group header row also shows that group's subtotals** —
257
+ the same aggregate functions applied to just the group's rows, displayed inline beside the group
258
+ label and count. No extra configuration is needed; subtotals appear whenever grouping and at least
259
+ one aggregated column are both active.
260
+
74
261
  ## Tree data
75
262
 
76
- Pass `treeConfig` to render rows as a hierarchical tree. The hierarchy lives on the flat row
77
- array via stable `id` / `parentId` accessors, so there are no nested `children` arrays and
78
- selection and editing keep working on the same indices.
263
+ Pass `treeConfig` to render rows as a hierarchical tree. Use stable `id` / `parentId` accessors
264
+ for an existing hierarchy, or `getPath` to generate display-only branches from each row.
79
265
 
80
266
  ```ts
81
267
  import { AgridTreeConfig } from '@thkl/agrid';
@@ -93,12 +279,90 @@ readonly provider = new AgridProvider<OrgRow>({
93
279
  });
94
280
  ```
95
281
 
282
+ For path-like values such as `01.01.0001`, return the ordered segments:
283
+
284
+ ```ts
285
+ const treeConfig: AgridTreeConfig<Row> = {
286
+ getPath: row => row.oz.split('.'),
287
+ nodeUuid: row => row.uuid,
288
+ formatPathSegment: ({ row, segment, level, leaf }) =>
289
+ leaf
290
+ ? `${segment} ${row.description}`
291
+ : `${segment} ${level === 0 ? row.areaLabel : row.groupLabel}`,
292
+ treeField: 'oz',
293
+ };
294
+ ```
295
+
296
+ This renders `01 / 01 / 0001, 0002`. Generated `01` branch rows are display-only; the leaf remains
297
+ the original datasource row. Its tree cell displays `0001`, while editing still uses the complete
298
+ `01.01.0001` value. `formatPathSegment` changes labels only; raw segments still control grouping,
299
+ expansion identity, and sort order. The callback receives the original `row`; shared branch nodes
300
+ use the first row encountered for that raw path prefix. `nodeUuid` uses the same source row and is
301
+ included in generated branch-node click events.
302
+
96
303
  The `treeField` column shows an indented expand/collapse twisty. Filtering and sorting behave as
97
304
  in a flat grid; with `keepAncestorsOnFilter` (default `true`) a match deep in the tree keeps its
98
305
  parents visible and force-opens the path to it. Tree mode takes precedence over grouping and
99
306
  disables pagination. Call `grid.expandAllNodes()` / `grid.collapseAllNodes()` to toggle the whole
100
307
  tree.
101
308
 
309
+ Tree mode can be combined with `masterDetail: true` and a `detailRenderer`. Detail expanders are
310
+ shown only for leaf rows; parent rows retain their tree expand/collapse control.
311
+
312
+ ## Standalone tree control
313
+
314
+ Use `<agrid-tree>` for the same parent-ID or path hierarchy without grid columns. It adds tree
315
+ keyboard navigation and optional single or multi-selection.
316
+
317
+ ```ts
318
+ readonly treeProvider = new AgridTreeProvider<OrgRow>({
319
+ datasource: new AgridDataSource(rows),
320
+ treeConfig: {
321
+ getId: row => row.id,
322
+ getParentId: row => row.parentId,
323
+ treeField: 'name',
324
+ defaultExpanded: true,
325
+ },
326
+ getDescription: row => row.role,
327
+ selection: 'single',
328
+ ariaLabel: 'Organization',
329
+ });
330
+ ```
331
+
332
+ ```html
333
+ <agrid-tree [provider]="treeProvider"
334
+ (nodeClick)="openNode($event)"
335
+ (nodeDoubleClicked)="openNode($event)"
336
+ (selectionChange)="selectionChanged($event)" />
337
+ ```
338
+
339
+ Row and generated path-branch clicks emit `AgridTreeNodeEvent<T>`. Branch events include their
340
+ configured or generated `uuid`. `expandAllNodes()` and `collapseAllNodes()` are public methods.
341
+ The tree uses the shared `--agrid-color-text`, `--agrid-color-text-muted`,
342
+ `--agrid-color-accent`, `--agrid-color-accent-subtle`, `--agrid-color-accent-fg`,
343
+ `--agrid-color-border`, `--agrid-color-bg`, and `--agrid-color-bg-muted` CSS variables.
344
+
345
+ ## Page selector
346
+
347
+ Use `<agrid-page-selector>` to navigate pages by previous/next button, dropdown, or typed ID.
348
+ All three interactions emit an `AgridPageItem<TId>` through the single `(selectPage)` output.
349
+
350
+ ```ts
351
+ readonly pages: AgridPageItem<number>[] = [
352
+ { id: 1, label: 'Cover' },
353
+ { id: 2, label: 'Measurements' },
354
+ { id: 3, label: 'Summary' },
355
+ ];
356
+ ```
357
+
358
+ ```html
359
+ <agrid-page-selector [items]="pages" [selectedId]="currentPageId()"
360
+ (selectPage)="currentPageId.set($event.id)" />
361
+ ```
362
+
363
+ IDs can be strings or numbers. Typed IDs are selected with Enter. Available inputs are
364
+ `disabled`, `previousLabel`, `nextLabel`, `inputLabel`, `menuLabel`, and `emptyText`.
365
+
102
366
  ## Saving edited rows
103
367
 
104
368
  Use `rowChanged` to send one request after the user edits one or more fields in a row: