@cocoar/data-grid 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,7 +1,556 @@
1
- # data-table
1
+ # @cocoar/data-grid
2
2
 
3
- This library was generated with [Nx](https://nx.dev).
3
+ AG Grid wrapper for the Cocoar Design System. Provides a fluent builder API, typed column factories, built-in cell renderers, and a design-token theme that supports light and dark mode out of the box.
4
4
 
5
- ## Running unit tests
5
+ ## Features
6
6
 
7
- Run `nx test data-table` to execute the unit tests.
7
+ - **Fluent Grid Builder** — configure columns, data, selection, sorting, filtering, and events through a chainable `CoarGridBuilder` API
8
+ - **Typed Column Factory** — create date, number, currency, boolean, tag, icon, and locale-aware date columns with a single method call
9
+ - **Built-in Cell Renderers** — `<coar-tag>`, `<coar-icon>`, and locale-aware date renderers available via factory methods
10
+ - **Cocoar Theme** — `ag-theme-cocoar` maps AG Grid CSS variables to `--coar-*` design tokens with automatic light/dark mode
11
+ - **Directive Binding** — `CoarDataGridDirective` binds a builder to `ag-grid-angular`, applies the theme class, and wires viewport events
12
+ - **Observable Data** — bind row data from an `Observable` with automatic loading state
13
+ - **Column State Persistence** — restore column widths, order, and visibility from saved state
14
+ - **External Filtering** — apply filters outside of AG Grid with observable triggers
15
+ - **Full-Row Editing** — enable row-level editing with a single builder call
16
+ - **Escape Hatch** — `option()` and `options()` let you set any AG Grid option directly
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pnpm add @cocoar/data-grid ag-grid-community ag-grid-angular
22
+ ```
23
+
24
+ ### Peer Dependencies
25
+
26
+ | Package | Version |
27
+ |---------|---------|
28
+ | `@angular/core` | `^21.0.0` |
29
+ | `@cocoar/localization` | `^0.1.0` |
30
+ | `@cocoar/ui` | `^0.1.0` |
31
+ | `ag-grid-angular` | `^35.0.0` |
32
+ | `ag-grid-community` | `^35.0.0` |
33
+ | `rxjs` | `^7.8.0` |
34
+
35
+ ## Quick Start
36
+
37
+ ### 1. Register AG Grid Modules
38
+
39
+ In your component or `app.config.ts`:
40
+
41
+ ```typescript
42
+ import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
43
+
44
+ ModuleRegistry.registerModules([AllCommunityModule]);
45
+ ```
46
+
47
+ ### 2. Import Styles
48
+
49
+ Add the Cocoar AG Grid theme to your application styles:
50
+
51
+ ```css
52
+ @import '@cocoar/data-grid/styles.css';
53
+ ```
54
+
55
+ ### 3. Use the Grid
56
+
57
+ ```typescript
58
+ import { Component } from '@angular/core';
59
+ import { AgGridAngular } from 'ag-grid-angular';
60
+ import { CoarGridBuilder, CoarDataGridDirective } from '@cocoar/data-grid';
61
+
62
+ interface User {
63
+ id: number;
64
+ name: string;
65
+ email: string;
66
+ }
67
+
68
+ @Component({
69
+ imports: [AgGridAngular, CoarDataGridDirective],
70
+ template: `
71
+ <ag-grid-angular
72
+ [coarDataGrid]="gridBuilder"
73
+ style="height: 400px;"
74
+ />
75
+ `,
76
+ })
77
+ export class UsersGridComponent {
78
+ readonly gridBuilder = CoarGridBuilder.create<User>()
79
+ .columns([
80
+ col => col.field('name').header('Name').flex(1).sortable(),
81
+ col => col.field('email').header('Email').flex(1),
82
+ ])
83
+ .rowData([
84
+ { id: 1, name: 'Alice', email: 'alice@example.com' },
85
+ { id: 2, name: 'Bob', email: 'bob@example.com' },
86
+ ])
87
+ .rowId(params => String(params.data.id));
88
+ }
89
+ ```
90
+
91
+ > The directive automatically applies the `ag-theme-cocoar` class and `display: flex` layout to the host element.
92
+
93
+ ## Grid Builder API
94
+
95
+ `CoarGridBuilder` is the main entry point. Create an instance with `CoarGridBuilder.create<TData>()`.
96
+
97
+ ### Properties
98
+
99
+ | Property | Type | Description |
100
+ |----------|------|-------------|
101
+ | `gridReady$` | `Observable<GridReadyEvent>` | Emits when the grid is ready |
102
+ | `api` | `GridApi \| undefined` | AG Grid API (available after grid ready) |
103
+
104
+ ### Methods
105
+
106
+ #### Column Configuration
107
+
108
+ | Method | Description |
109
+ |--------|-------------|
110
+ | `columns(defs)` | Define columns using builders or factory functions |
111
+ | `defaultColDef(def)` | Set default column definition applied to all columns. Accepts an object or a builder function |
112
+
113
+ #### Data
114
+
115
+ | Method | Description |
116
+ |--------|-------------|
117
+ | `rowData(data)` | Set row data from a static array (or `null`) |
118
+ | `rowData$(data$)` | Set row data from an `Observable`. Emitting `null`/`undefined` shows the loading overlay |
119
+ | `rowId(fn)` | Set a `GetRowIdFunc` for immutable data updates |
120
+
121
+ #### Selection
122
+
123
+ | Method | Description |
124
+ |--------|-------------|
125
+ | `rowSelection(mode)` | Enable `'single'` or `'multiple'` row selection |
126
+
127
+ #### Row Styling
128
+
129
+ | Method | Description |
130
+ |--------|-------------|
131
+ | `rowClassRules(rules)` | Apply conditional CSS classes to rows |
132
+ | `rowClass(fn)` | Set a function that returns dynamic class name(s) per row |
133
+
134
+ #### Sorting
135
+
136
+ | Method | Description |
137
+ |--------|-------------|
138
+ | `defaultSort(field, direction)` | Set initial sort column and `'asc'` or `'desc'` direction |
139
+ | `sortFunction(fn)` | Set a custom post-sort function to reorder rows after AG Grid sorts |
140
+ | `updateSortAndFilterWhen(trigger$)` | Re-trigger sort and filter when the observable emits |
141
+
142
+ #### Column State
143
+
144
+ | Method | Description |
145
+ |--------|-------------|
146
+ | `columnState(state)` | Restore column widths, order, and visibility from an array or observable |
147
+
148
+ #### Tree / Group Data
149
+
150
+ | Method | Description |
151
+ |--------|-------------|
152
+ | `openRows(openRows$)` | Set which parent rows are expanded (observable of row IDs) |
153
+
154
+ #### Editing
155
+
156
+ | Method | Description |
157
+ |--------|-------------|
158
+ | `fullRowEdit()` | Enable full-row editing mode |
159
+ | `stopEditingWhenCellsLoseFocus()` | Stop editing when cells lose focus |
160
+
161
+ #### Resize
162
+
163
+ | Method | Description |
164
+ |--------|-------------|
165
+ | `shiftResizeMode()` | Enable shift-key column resize mode |
166
+
167
+ #### Animation
168
+
169
+ | Method | Description |
170
+ |--------|-------------|
171
+ | `animateRows()` | Enable row animation (enabled by default) |
172
+
173
+ #### Events
174
+
175
+ | Method | Description |
176
+ |--------|-------------|
177
+ | `onGridReady(handler)` | Handle grid ready event |
178
+ | `onRowClicked(handler)` | Handle row click |
179
+ | `onRowDoubleClicked(handler)` | Handle row double-click |
180
+ | `onCellClicked(handler)` | Handle cell click |
181
+ | `onCellDoubleClicked(handler)` | Handle cell double-click |
182
+ | `onCellContextMenu(handler)` | Handle cell right-click (Ctrl+click passes through to the browser) |
183
+ | `onViewportClick(handler)` | Handle click on empty grid area (wired by directive) |
184
+ | `onViewportContextMenu(handler)` | Handle right-click on empty grid area (wired by directive) |
185
+ | `onGridSizeChanged(handler)` | Handle grid size change |
186
+
187
+ #### External Filtering
188
+
189
+ | Method | Description |
190
+ |--------|-------------|
191
+ | `externalFilter(doesFilterPass, isFilterPresent?)` | Set an external filter function |
192
+ | `updateExternalFilterWhen(trigger$)` | Re-trigger the external filter when the observable emits |
193
+
194
+ #### Escape Hatch
195
+
196
+ | Method | Description |
197
+ |--------|-------------|
198
+ | `option(key, value)` | Set any single `GridOptions` property |
199
+ | `options(opts)` | Merge a partial `GridOptions` object |
200
+
201
+ ## Column Builder API
202
+
203
+ `CoarGridColumnBuilder` configures individual columns with a fluent API. You rarely instantiate it directly — it is provided by the factory function in `columns()`.
204
+
205
+ | Method | Description |
206
+ |--------|-------------|
207
+ | `field(name)` | Set the data field name |
208
+ | `header(text)` | Set the column header text |
209
+ | `headerTooltip(text)` | Set header tooltip |
210
+ | `width(px, minWidth?, maxWidth?)` | Set width in pixels with optional min/max |
211
+ | `fixedWidth(px)` | Set width, minWidth, and maxWidth to the same value |
212
+ | `minWidth(px)` | Set minimum width |
213
+ | `maxWidth(px)` | Set maximum width |
214
+ | `flex(n)` | Set flex grow factor (default: `1`) |
215
+ | `sortable()` | Enable sorting |
216
+ | `resizable()` | Enable resizing (enabled by default) |
217
+ | `hidden()` | Hide the column |
218
+ | `pinned(side)` | Pin to `'left'`, `'right'`, or `null` |
219
+ | `lockPosition(value?)` | Lock column position |
220
+ | `cellRenderer(component, params?)` | Set a custom cell renderer Angular component |
221
+ | `cellRendererParams(params)` | Set cell renderer parameters |
222
+ | `cellRendererConfig(component, config)` | Set cell renderer with a `config` object |
223
+ | `valueFormatter(fn)` | Format the displayed value |
224
+ | `valueGetter(fn)` | Transform data before display |
225
+ | `cellClass(value)` | Set CSS class (string, array, or function) |
226
+ | `cellStyle(value)` | Set CSS style (object or function) |
227
+ | `classRule(className, condition)` | Add a conditional CSS class rule |
228
+ | `tooltip(value?)` | Show tooltip — field value by default, or a string/function |
229
+ | `onCellDoubleClicked(handler)` | Handle cell double-click |
230
+ | `filter(value?)` | Enable filtering (boolean or filter type string) |
231
+ | `filterParams(params)` | Set filter parameters |
232
+ | `quickFilter(fn)` | Set quick-filter text extractor, or `false` to exclude |
233
+ | `comparator(fn)` | Set a custom sort comparator |
234
+ | `rowDrag(value?)` | Enable row drag on this column |
235
+ | `option(key, value)` | Set any single `ColDef` property |
236
+ | `customize(fn)` | Apply custom modifications to the `ColDef` |
237
+ | `build()` | Build and return the AG Grid `ColDef` |
238
+
239
+ ## Column Factory Methods
240
+
241
+ When you pass a function to `columns()`, it receives a `CoarGridColumnFactory<TData>`. The factory provides shorthand methods for common column types.
242
+
243
+ ### `field(fieldName)`
244
+
245
+ Creates a plain column builder — identical to `new CoarGridColumnBuilder(fieldName)`.
246
+
247
+ ```typescript
248
+ col => col.field('name').header('Name').flex(1).sortable()
249
+ ```
250
+
251
+ ### `date(fieldName, format?)`
252
+
253
+ Creates a date column with a `valueFormatter`. The `format` parameter accepts:
254
+
255
+ | Value | Output |
256
+ |-------|--------|
257
+ | `'short'` (default) | `date.toLocaleDateString()` |
258
+ | `'long'` | Full date with month name |
259
+ | `'datetime'` | `date.toLocaleString()` |
260
+ | `Intl.DateTimeFormatOptions` | Custom Intl formatting |
261
+ | `(date: Date) => string` | Custom formatter function |
262
+
263
+ ```typescript
264
+ col => col.date('createdAt', 'long').header('Created').width(180)
265
+ ```
266
+
267
+ ### `number(fieldName, decimals?)`
268
+
269
+ Creates a right-aligned number column formatted with `toLocaleString`. Default `decimals` is `0`.
270
+
271
+ ```typescript
272
+ col => col.number('score', 2).header('Score').width(100)
273
+ ```
274
+
275
+ ### `currency(fieldName, currency?)`
276
+
277
+ Creates a right-aligned currency column. Default currency is `'USD'`.
278
+
279
+ ```typescript
280
+ col => col.currency('price', 'EUR').header('Price').width(120)
281
+ ```
282
+
283
+ ### `boolean(fieldName, options?)`
284
+
285
+ Creates a column that displays `'Yes'`/`'No'` (or custom values).
286
+
287
+ ```typescript
288
+ col => col.boolean('active', { trueValue: 'Active', falseValue: 'Inactive' }).header('Status')
289
+ ```
290
+
291
+ ### `tag(fieldName, config?)`
292
+
293
+ Creates a column that renders values as `<coar-tag>` elements. Sortable by default.
294
+
295
+ Supports string values (split by separator), arrays, and object arrays.
296
+
297
+ **`TagCellRendererConfig`:**
298
+
299
+ | Property | Type | Default | Description |
300
+ |----------|------|---------|-------------|
301
+ | `separator` | `string` | `','` | Delimiter to split string values |
302
+ | `variant` | `TagVariant` | — | Default variant for all tags |
303
+ | `variantMap` | `Record<string, TagVariant>` | — | Map tag values to variants |
304
+ | `size` | `'s' \| 'm' \| 'l'` | `'s'` | Tag size |
305
+ | `i18nPrefix` | `string` | — | Prefix for i18n translation keys |
306
+ | `labelProperty` | `string` | — | Property name when values are objects |
307
+
308
+ ```typescript
309
+ col => col.tag('roles', {
310
+ separator: ',',
311
+ size: 's',
312
+ variantMap: { admin: 'primary', editor: 'info', viewer: 'neutral' },
313
+ })
314
+ ```
315
+
316
+ ### `icon(fieldName, config?)`
317
+
318
+ Creates a column that renders the cell value as a `<coar-icon>`.
319
+
320
+ **`IconCellRendererConfig`:**
321
+
322
+ | Property | Type | Default | Description |
323
+ |----------|------|---------|-------------|
324
+ | `size` | `CoarIconSize` | `'s'` | Icon size (`'xs'`, `'s'`, `'m'`, `'l'`, `'xl'`) |
325
+ | `source` | `string` | — | Icon source registry key |
326
+ | `color` | `string` | `'inherit'` | CSS color value |
327
+ | `onClick` | `(params) => void` | — | Click handler (makes the icon clickable) |
328
+
329
+ ```typescript
330
+ col => col.icon('fileType', { size: 'm', color: 'var(--coar-color-primary)' })
331
+ ```
332
+
333
+ ### `localDate(fieldName, config?)`
334
+
335
+ Creates a date column that renders via `CoarDatePipe` from `@cocoar/localization` for full locale integration. Sortable by default.
336
+
337
+ **`DateCellRendererConfig`:**
338
+
339
+ | Property | Type | Default | Description |
340
+ |----------|------|---------|-------------|
341
+ | `showSeconds` | `boolean` | — | Include seconds in time display |
342
+ | `customFormat` | `string` | — | Custom Angular date format string |
343
+
344
+ ```typescript
345
+ col => col.localDate('updatedAt', { showSeconds: false }).header('Updated')
346
+ ```
347
+
348
+ ## Directive
349
+
350
+ `CoarDataGridDirective` connects a `CoarGridBuilder` to an `ag-grid-angular` instance.
351
+
352
+ **Selector:** `ag-grid-angular[coarDataGrid]`
353
+ **Export:** `coarDataGrid`
354
+
355
+ **Host bindings applied automatically:**
356
+ - CSS class: `ag-theme-cocoar`
357
+ - Style: `display: flex; flex-direction: column; flex-grow: 1`
358
+
359
+ The directive also forwards viewport click and context-menu events from the grid's empty area to the builder's `onViewportClick` / `onViewportContextMenu` handlers.
360
+
361
+ ```html
362
+ <ag-grid-angular
363
+ [coarDataGrid]="gridBuilder"
364
+ style="height: 500px;"
365
+ />
366
+ ```
367
+
368
+ ## Theming
369
+
370
+ ### CSS Theme
371
+
372
+ Import the theme CSS in your application styles:
373
+
374
+ ```css
375
+ @import '@cocoar/data-grid/styles.css';
376
+ ```
377
+
378
+ The `ag-theme-cocoar` class maps AG Grid CSS variables to Cocoar design tokens. Light mode is the default; dark mode activates when a `.dark-mode` class is present on the grid or a parent element.
379
+
380
+ **Key CSS variable mappings:**
381
+
382
+ | AG Grid Variable | Cocoar Token | Purpose |
383
+ |-----------------|-------------|---------|
384
+ | `--ag-background-color` | `--coar-background-neutral-primary` | Grid background |
385
+ | `--ag-header-background-color` | `--coar-background-neutral-secondary` | Header background |
386
+ | `--ag-row-hover-color` | `--coar-background-neutral-tertiary` | Row hover |
387
+ | `--ag-selected-row-background-color` | `--coar-background-accent-tertiary` | Selected row |
388
+ | `--ag-border-color` | `--coar-border-neutral-tertiary` | Borders |
389
+ | `--ag-foreground-color` | `--coar-text-neutral-primary` | Text |
390
+ | `--ag-header-foreground-color` | `--coar-text-neutral-secondary` | Header text |
391
+ | `--ag-font-family` | `--coar-font-family-body` | Typography |
392
+ | `--ag-checkbox-checked-color` | `--coar-background-accent-primary` | Checkbox |
393
+
394
+ **Utility CSS classes** available inside `ag-theme-cocoar`:
395
+ - `.clickable` — pointer cursor with accent color
396
+ - `.text-right` — right-aligned cell content
397
+ - `.text-center` — center-aligned cell content
398
+
399
+ ### JavaScript Theme
400
+
401
+ The library also exports a JavaScript theme for AG Grid's v33+ theming API (based on `themeQuartz`):
402
+
403
+ ```typescript
404
+ import { cocoarTheme, createCocoarTheme } from '@cocoar/data-grid';
405
+
406
+ // Use the pre-configured instance (applied by the builder automatically)
407
+ const theme = cocoarTheme;
408
+
409
+ // Or create a customized instance
410
+ const custom = createCocoarTheme();
411
+ ```
412
+
413
+ The builder applies `cocoarTheme` by default — you only need this export if you use AG Grid directly without the builder.
414
+
415
+ ## Examples
416
+
417
+ ### Row Selection
418
+
419
+ ```typescript
420
+ const grid = CoarGridBuilder.create<User>()
421
+ .columns([
422
+ col => col.field('name').header('Name').flex(1),
423
+ col => col.field('email').header('Email').flex(1),
424
+ ])
425
+ .rowData(users)
426
+ .rowSelection('single')
427
+ .onRowClicked(event => {
428
+ console.log('Selected:', event.data);
429
+ });
430
+ ```
431
+
432
+ ### Conditional Row and Cell Styling
433
+
434
+ ```typescript
435
+ const grid = CoarGridBuilder.create<Task>()
436
+ .columns([
437
+ col => col.field('title').header('Title').flex(1),
438
+ col => col.field('priority').header('Priority').width(100)
439
+ .classRule('high-priority', params => params.value === 'high')
440
+ .cellStyle(params =>
441
+ params.value === 'high' ? { fontWeight: '600' } : null
442
+ ),
443
+ ])
444
+ .rowData(tasks)
445
+ .rowClassRules({
446
+ 'overdue': params => params.data?.dueDate < new Date(),
447
+ });
448
+ ```
449
+
450
+ ### Typed Columns
451
+
452
+ ```typescript
453
+ const grid = CoarGridBuilder.create<Invoice>()
454
+ .columns([
455
+ col => col.field('number').header('#').fixedWidth(80),
456
+ col => col.date('issuedAt', 'long').header('Issued').width(180),
457
+ col => col.number('quantity', 0).header('Qty').width(80),
458
+ col => col.currency('total', 'EUR').header('Total').width(120),
459
+ col => col.boolean('paid').header('Paid').width(80),
460
+ ])
461
+ .rowData(invoices);
462
+ ```
463
+
464
+ ### Tag and Icon Cell Renderers
465
+
466
+ ```typescript
467
+ const grid = CoarGridBuilder.create<User>()
468
+ .columns([
469
+ col => col.field('name').header('Name').flex(1),
470
+ col => col.tag('roles', {
471
+ variantMap: { admin: 'primary', editor: 'info', viewer: 'neutral' },
472
+ size: 's',
473
+ }).header('Roles').flex(1),
474
+ col => col.icon('status', {
475
+ size: 's',
476
+ onClick: params => alert(`Clicked ${params.data.name}`),
477
+ }).header('').fixedWidth(50),
478
+ ])
479
+ .rowData(users);
480
+ ```
481
+
482
+ ### Observable Data Binding
483
+
484
+ ```typescript
485
+ readonly users$ = inject(UserService).getUsers();
486
+
487
+ readonly grid = CoarGridBuilder.create<User>()
488
+ .columns([
489
+ col => col.field('name').header('Name').flex(1),
490
+ col => col.field('email').header('Email').flex(1),
491
+ ])
492
+ .rowData$(this.users$)
493
+ .rowId(params => String(params.data.id));
494
+ ```
495
+
496
+ ### Sorting and Custom Comparators
497
+
498
+ ```typescript
499
+ const grid = CoarGridBuilder.create<Item>()
500
+ .columns([
501
+ col => col.field('name').header('Name').flex(1).sortable()
502
+ .comparator((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })),
503
+ col => col.field('priority').header('Priority').width(120).sortable()
504
+ .comparator((a, b) => priorityOrder[a] - priorityOrder[b]),
505
+ ])
506
+ .rowData(items)
507
+ .defaultSort('priority', 'asc');
508
+ ```
509
+
510
+ ### Context Menu Integration
511
+
512
+ ```typescript
513
+ const grid = CoarGridBuilder.create<File>()
514
+ .columns([...])
515
+ .rowData(files)
516
+ .onCellContextMenu(event => {
517
+ const mouseEvent = event.event as MouseEvent;
518
+ openContextMenu(mouseEvent, event.data);
519
+ })
520
+ .onViewportContextMenu(($event, api) => {
521
+ openBackgroundMenu($event);
522
+ });
523
+ ```
524
+
525
+ ### Full-Row Editing
526
+
527
+ ```typescript
528
+ const grid = CoarGridBuilder.create<Record>()
529
+ .columns([
530
+ col => col.field('name').header('Name').flex(1).option('editable', true),
531
+ col => col.field('value').header('Value').flex(1).option('editable', true),
532
+ ])
533
+ .rowData(records)
534
+ .fullRowEdit()
535
+ .stopEditingWhenCellsLoseFocus();
536
+ ```
537
+
538
+ ### External Filtering
539
+
540
+ ```typescript
541
+ readonly searchTerm = signal('');
542
+
543
+ readonly grid = CoarGridBuilder.create<Item>()
544
+ .columns([...])
545
+ .rowData(items)
546
+ .externalFilter(node => {
547
+ const term = this.searchTerm().toLowerCase();
548
+ if (!term) return true;
549
+ return node.data.name.toLowerCase().includes(term);
550
+ })
551
+ .updateExternalFilterWhen(toObservable(this.searchTerm));
552
+ ```
553
+
554
+ ## License
555
+
556
+ Apache-2.0