@toolbox-web/grid-angular 0.11.0 → 0.12.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/fesm2022/toolbox-web-grid-angular-features-selection.mjs +34 -1
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular.mjs +230 -19
- package/fesm2022/toolbox-web-grid-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/toolbox-web-grid-angular-features-selection.d.ts +37 -2
- package/types/toolbox-web-grid-angular-features-selection.d.ts.map +1 -1
- package/types/toolbox-web-grid-angular.d.ts +122 -2
- package/types/toolbox-web-grid-angular.d.ts.map +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject, ElementRef, signal } from '@angular/core';
|
|
1
|
+
import { inject, ElementRef, DestroyRef, signal } from '@angular/core';
|
|
2
2
|
import { registerFeature } from '@toolbox-web/grid-angular';
|
|
3
3
|
import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';
|
|
4
4
|
|
|
@@ -85,10 +85,40 @@ registerFeature('selection', (config) => {
|
|
|
85
85
|
*/
|
|
86
86
|
function injectGridSelection() {
|
|
87
87
|
const elementRef = inject(ElementRef);
|
|
88
|
+
const destroyRef = inject(DestroyRef);
|
|
88
89
|
const isReady = signal(false, ...(ngDevMode ? [{ debugName: "isReady" }] : []));
|
|
90
|
+
// Reactive selection state
|
|
91
|
+
const selectionSignal = signal(null, ...(ngDevMode ? [{ debugName: "selectionSignal" }] : []));
|
|
92
|
+
const selectedRowIndicesSignal = signal([], ...(ngDevMode ? [{ debugName: "selectedRowIndicesSignal" }] : []));
|
|
89
93
|
// Lazy discovery: cached grid reference
|
|
90
94
|
let cachedGrid = null;
|
|
91
95
|
let readyPromiseStarted = false;
|
|
96
|
+
let listenerAttached = false;
|
|
97
|
+
/**
|
|
98
|
+
* Handle selection-change events from the grid.
|
|
99
|
+
* Updates both reactive signals.
|
|
100
|
+
*/
|
|
101
|
+
const onSelectionChange = (e) => {
|
|
102
|
+
const detail = e.detail;
|
|
103
|
+
const plugin = getPlugin();
|
|
104
|
+
if (plugin) {
|
|
105
|
+
selectionSignal.set(plugin.getSelection());
|
|
106
|
+
selectedRowIndicesSignal.set(detail.mode === 'row' ? plugin.getSelectedRowIndices() : []);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Attach the selection-change event listener to the grid element.
|
|
111
|
+
* Called once when the grid is first discovered.
|
|
112
|
+
*/
|
|
113
|
+
const attachListener = (grid) => {
|
|
114
|
+
if (listenerAttached)
|
|
115
|
+
return;
|
|
116
|
+
listenerAttached = true;
|
|
117
|
+
grid.addEventListener('selection-change', onSelectionChange);
|
|
118
|
+
destroyRef.onDestroy(() => {
|
|
119
|
+
grid.removeEventListener('selection-change', onSelectionChange);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
92
122
|
/**
|
|
93
123
|
* Lazily find the grid element. Called on each method invocation.
|
|
94
124
|
* Caches the reference once found and triggers ready() check.
|
|
@@ -99,6 +129,7 @@ function injectGridSelection() {
|
|
|
99
129
|
const grid = elementRef.nativeElement.querySelector('tbw-grid');
|
|
100
130
|
if (grid) {
|
|
101
131
|
cachedGrid = grid;
|
|
132
|
+
attachListener(grid);
|
|
102
133
|
// Start ready() check only once
|
|
103
134
|
if (!readyPromiseStarted) {
|
|
104
135
|
readyPromiseStarted = true;
|
|
@@ -112,6 +143,8 @@ function injectGridSelection() {
|
|
|
112
143
|
};
|
|
113
144
|
return {
|
|
114
145
|
isReady: isReady.asReadonly(),
|
|
146
|
+
selection: selectionSignal.asReadonly(),
|
|
147
|
+
selectedRowIndices: selectedRowIndicesSignal.asReadonly(),
|
|
115
148
|
selectAll: () => {
|
|
116
149
|
const plugin = getPlugin();
|
|
117
150
|
if (!plugin) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-selection.mjs","sources":["../../../../libs/grid-angular/features/selection/src/index.ts","../../../../libs/grid-angular/features/selection/src/toolbox-web-grid-angular-features-selection.ts"],"sourcesContent":["/**\n * Selection feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `selection` input on Grid directive.\n * Also exports `injectGridSelection()` for programmatic selection control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/selection';\n *\n * <tbw-grid [selection]=\"'range'\" />\n * ```\n *\n * @example Using injectGridSelection\n * ```typescript\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({...})\n * export class MyComponent {\n * private selection = injectGridSelection<Employee>();\n *\n * selectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelected() {\n * return this.selection.getSelection();\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { SelectionPlugin, type CellRange, type SelectionResult } from '@toolbox-web/grid/plugins/selection';\n\nregisterFeature('selection', (config) => {\n // Handle shorthand: 'cell', 'row', 'range'\n if (config === 'cell' || config === 'row' || config === 'range') {\n return new SelectionPlugin({ mode: config });\n }\n // Full config object\n return new SelectionPlugin(config ?? undefined);\n});\n\n/**\n * Selection methods returned from injectGridSelection.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n * This ensures it works with lazy-rendered tabs, conditional rendering, etc.\n */\nexport interface SelectionMethods {\n /**\n * Select all rows (row mode) or all cells (range mode).\n */\n selectAll: () => void;\n\n /**\n * Clear all selection.\n */\n clearSelection: () => void;\n\n /**\n * Get the current selection state.\n * Use this to derive selected rows, indices, etc.\n */\n getSelection: () => SelectionResult | null;\n\n /**\n * Check if a specific cell is selected.\n */\n isCellSelected: (row: number, col: number) => boolean;\n\n /**\n * Set selection ranges programmatically.\n */\n setRanges: (ranges: CellRange[]) => void;\n\n /**\n * Signal indicating if grid is ready.\n * The grid is discovered lazily, so this updates when first method call succeeds.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic selection control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization. This ensures it works reliably with:\n * - Lazy-rendered tabs\n * - Conditional rendering (*ngIf)\n * - Dynamic component loading\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/selection';\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handleSelectAll()\">Select All</button>\n * <tbw-grid [rows]=\"rows\" [selection]=\"'range'\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * selection = injectGridSelection();\n *\n * handleSelectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelectedRows() {\n * const selection = this.selection.getSelection();\n * if (!selection) return [];\n * // Derive rows from selection.ranges as needed\n * }\n * }\n * ```\n */\nexport function injectGridSelection<TRow = unknown>(): SelectionMethods {\n const elementRef = inject(ElementRef);\n const isReady = signal(false);\n\n // Lazy discovery: cached grid reference\n let cachedGrid: DataGridElement<TRow> | null = null;\n let readyPromiseStarted = false;\n\n /**\n * Lazily find the grid element. Called on each method invocation.\n * Caches the reference once found and triggers ready() check.\n */\n const getGrid = (): DataGridElement<TRow> | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement<TRow> | null;\n if (grid) {\n cachedGrid = grid;\n // Start ready() check only once\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): SelectionPlugin | undefined => {\n return getGrid()?.getPlugin(SelectionPlugin);\n };\n\n return {\n isReady: isReady.asReadonly(),\n\n selectAll: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:selection] SelectionPlugin not found.\\n\\n` +\n ` → Enable selection on the grid:\\n` +\n ` <tbw-grid [selection]=\"'range'\" />`,\n );\n return;\n }\n const grid = getGrid();\n // Cast to any to access protected config\n const mode = (plugin as any).config?.mode;\n\n if (mode === 'row') {\n const rowCount = grid?.rows?.length ?? 0;\n const allIndices = new Set<number>();\n for (let i = 0; i < rowCount; i++) allIndices.add(i);\n (plugin as any).selected = allIndices;\n (plugin as any).requestAfterRender?.();\n } else if (mode === 'range') {\n const rowCount = grid?.rows?.length ?? 0;\n const colCount = (grid as any)?._columns?.length ?? 0;\n if (rowCount > 0 && colCount > 0) {\n plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: rowCount - 1, col: colCount - 1 } }]);\n }\n }\n },\n\n clearSelection: () => {\n getPlugin()?.clearSelection();\n },\n\n getSelection: () => {\n return getPlugin()?.getSelection() ?? null;\n },\n\n isCellSelected: (row: number, col: number) => {\n return getPlugin()?.isCellSelected(row, col) ?? false;\n },\n\n setRanges: (ranges: CellRange[]) => {\n getPlugin()?.setRanges(ranges);\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toolbox-web-grid-angular-features-selection.mjs","sources":["../../../../libs/grid-angular/features/selection/src/index.ts","../../../../libs/grid-angular/features/selection/src/toolbox-web-grid-angular-features-selection.ts"],"sourcesContent":["/**\n * Selection feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `selection` input on Grid directive.\n * Also exports `injectGridSelection()` for programmatic selection control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/selection';\n *\n * <tbw-grid [selection]=\"'range'\" />\n * ```\n *\n * @example Using injectGridSelection\n * ```typescript\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({...})\n * export class MyComponent {\n * private selection = injectGridSelection<Employee>();\n *\n * selectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelected() {\n * return this.selection.getSelection();\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { DestroyRef, ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport {\n SelectionPlugin,\n type CellRange,\n type SelectionChangeDetail,\n type SelectionResult,\n} from '@toolbox-web/grid/plugins/selection';\n\nregisterFeature('selection', (config) => {\n // Handle shorthand: 'cell', 'row', 'range'\n if (config === 'cell' || config === 'row' || config === 'range') {\n return new SelectionPlugin({ mode: config });\n }\n // Full config object\n return new SelectionPlugin(config ?? undefined);\n});\n\n/**\n * Selection methods returned from injectGridSelection.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n * This ensures it works with lazy-rendered tabs, conditional rendering, etc.\n */\nexport interface SelectionMethods {\n /**\n * Select all rows (row mode) or all cells (range mode).\n */\n selectAll: () => void;\n\n /**\n * Clear all selection.\n */\n clearSelection: () => void;\n\n /**\n * Get the current selection state (imperative, point-in-time snapshot).\n * For reactive selection state, use the `selection` signal instead.\n */\n getSelection: () => SelectionResult | null;\n\n /**\n * Check if a specific cell is selected.\n */\n isCellSelected: (row: number, col: number) => boolean;\n\n /**\n * Set selection ranges programmatically.\n */\n setRanges: (ranges: CellRange[]) => void;\n\n /**\n * Reactive selection state. Updates automatically whenever the selection changes.\n * Null when no SelectionPlugin is active or no selection has been made yet.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selection()?.ranges?.length ?? 0 }} cells selected\n *\n * // In computed:\n * readonly hasSelection = computed(() => (this.selection.selection()?.ranges?.length ?? 0) > 0);\n * ```\n */\n selection: Signal<SelectionResult | null>;\n\n /**\n * Reactive selected row indices (sorted ascending). Updates automatically.\n * Convenience signal for row-mode selection — returns `[]` in cell/range modes\n * or when nothing is selected.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selectedRowIndices().length }} rows selected\n *\n * // In computed:\n * readonly selectedRows = computed(() =>\n * this.selection.selectedRowIndices().map(i => this.rows[i])\n * );\n * ```\n */\n selectedRowIndices: Signal<number[]>;\n\n /**\n * Signal indicating if grid is ready.\n * The grid is discovered lazily, so this updates when first method call succeeds.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic selection control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization. This ensures it works reliably with:\n * - Lazy-rendered tabs\n * - Conditional rendering (*ngIf)\n * - Dynamic component loading\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/selection';\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handleSelectAll()\">Select All</button>\n * <tbw-grid [rows]=\"rows\" [selection]=\"'range'\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * selection = injectGridSelection();\n *\n * handleSelectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelectedRows() {\n * const selection = this.selection.getSelection();\n * if (!selection) return [];\n * // Derive rows from selection.ranges as needed\n * }\n * }\n * ```\n */\nexport function injectGridSelection<TRow = unknown>(): SelectionMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n // Reactive selection state\n const selectionSignal = signal<SelectionResult | null>(null);\n const selectedRowIndicesSignal = signal<number[]>([]);\n\n // Lazy discovery: cached grid reference\n let cachedGrid: DataGridElement<TRow> | null = null;\n let readyPromiseStarted = false;\n let listenerAttached = false;\n\n /**\n * Handle selection-change events from the grid.\n * Updates both reactive signals.\n */\n const onSelectionChange = (e: Event): void => {\n const detail = (e as CustomEvent<SelectionChangeDetail>).detail;\n const plugin = getPlugin();\n if (plugin) {\n selectionSignal.set(plugin.getSelection());\n selectedRowIndicesSignal.set(detail.mode === 'row' ? plugin.getSelectedRowIndices() : []);\n }\n };\n\n /**\n * Attach the selection-change event listener to the grid element.\n * Called once when the grid is first discovered.\n */\n const attachListener = (grid: DataGridElement<TRow>): void => {\n if (listenerAttached) return;\n listenerAttached = true;\n\n grid.addEventListener('selection-change', onSelectionChange);\n\n destroyRef.onDestroy(() => {\n grid.removeEventListener('selection-change', onSelectionChange);\n });\n };\n\n /**\n * Lazily find the grid element. Called on each method invocation.\n * Caches the reference once found and triggers ready() check.\n */\n const getGrid = (): DataGridElement<TRow> | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement<TRow> | null;\n if (grid) {\n cachedGrid = grid;\n attachListener(grid);\n // Start ready() check only once\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): SelectionPlugin | undefined => {\n return getGrid()?.getPlugin(SelectionPlugin);\n };\n\n return {\n isReady: isReady.asReadonly(),\n selection: selectionSignal.asReadonly(),\n selectedRowIndices: selectedRowIndicesSignal.asReadonly(),\n\n selectAll: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:selection] SelectionPlugin not found.\\n\\n` +\n ` → Enable selection on the grid:\\n` +\n ` <tbw-grid [selection]=\"'range'\" />`,\n );\n return;\n }\n const grid = getGrid();\n // Cast to any to access protected config\n const mode = (plugin as any).config?.mode;\n\n if (mode === 'row') {\n const rowCount = grid?.rows?.length ?? 0;\n const allIndices = new Set<number>();\n for (let i = 0; i < rowCount; i++) allIndices.add(i);\n (plugin as any).selected = allIndices;\n (plugin as any).requestAfterRender?.();\n } else if (mode === 'range') {\n const rowCount = grid?.rows?.length ?? 0;\n const colCount = (grid as any)?._columns?.length ?? 0;\n if (rowCount > 0 && colCount > 0) {\n plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: rowCount - 1, col: colCount - 1 } }]);\n }\n }\n },\n\n clearSelection: () => {\n getPlugin()?.clearSelection();\n },\n\n getSelection: () => {\n return getPlugin()?.getSelection() ?? null;\n },\n\n isCellSelected: (row: number, col: number) => {\n return getPlugin()?.isCellSelected(row, col) ?? false;\n },\n\n setRanges: (ranges: CellRange[]) => {\n getPlugin()?.setRanges(ranges);\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -1413,6 +1413,10 @@ class GridAdapter {
|
|
|
1413
1413
|
if (config.editor && isComponentClass(config.editor)) {
|
|
1414
1414
|
processedConfig.editor = this.createComponentEditor(config.editor);
|
|
1415
1415
|
}
|
|
1416
|
+
// Convert filterPanelRenderer component class to function
|
|
1417
|
+
if (config.filterPanelRenderer && isComponentClass(config.filterPanelRenderer)) {
|
|
1418
|
+
processedConfig.filterPanelRenderer = this.createComponentFilterPanelRenderer(config.filterPanelRenderer);
|
|
1419
|
+
}
|
|
1416
1420
|
processed[type] = processedConfig;
|
|
1417
1421
|
}
|
|
1418
1422
|
return processed;
|
|
@@ -1457,12 +1461,31 @@ class GridAdapter {
|
|
|
1457
1461
|
// This is important when only an editor template is provided (no view template)
|
|
1458
1462
|
return undefined;
|
|
1459
1463
|
}
|
|
1464
|
+
// Cell cache for this column - maps cell element to its view ref and root node.
|
|
1465
|
+
// When the grid recycles pool elements during scroll, the same cellEl is reused
|
|
1466
|
+
// for different row data. By caching per cellEl, we reuse the Angular view and
|
|
1467
|
+
// just update its context instead of creating a new embedded view every time.
|
|
1468
|
+
// This matches what React and Vue adapters do with their cell caches.
|
|
1469
|
+
const cellCache = new WeakMap();
|
|
1460
1470
|
return (ctx) => {
|
|
1461
1471
|
// Skip rendering if the cell is in editing mode
|
|
1462
1472
|
// This prevents the renderer from overwriting the editor when the grid re-renders
|
|
1463
1473
|
if (ctx.cellEl?.classList.contains('editing')) {
|
|
1464
1474
|
return null;
|
|
1465
1475
|
}
|
|
1476
|
+
const cellEl = ctx.cellEl;
|
|
1477
|
+
if (cellEl) {
|
|
1478
|
+
const cached = cellCache.get(cellEl);
|
|
1479
|
+
if (cached) {
|
|
1480
|
+
// Reuse existing view - just update context and re-run change detection
|
|
1481
|
+
cached.viewRef.context.$implicit = ctx.value;
|
|
1482
|
+
cached.viewRef.context.value = ctx.value;
|
|
1483
|
+
cached.viewRef.context.row = ctx.row;
|
|
1484
|
+
cached.viewRef.context.column = ctx.column;
|
|
1485
|
+
cached.viewRef.detectChanges();
|
|
1486
|
+
return cached.rootNode;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1466
1489
|
// Create the context for the template
|
|
1467
1490
|
const context = {
|
|
1468
1491
|
$implicit: ctx.value,
|
|
@@ -1477,6 +1500,10 @@ class GridAdapter {
|
|
|
1477
1500
|
viewRef.detectChanges();
|
|
1478
1501
|
// Get the first root node (the component's host element)
|
|
1479
1502
|
const rootNode = viewRef.rootNodes[0];
|
|
1503
|
+
// Cache for reuse on scroll recycles
|
|
1504
|
+
if (cellEl) {
|
|
1505
|
+
cellCache.set(cellEl, { viewRef, rootNode });
|
|
1506
|
+
}
|
|
1480
1507
|
return rootNode;
|
|
1481
1508
|
};
|
|
1482
1509
|
}
|
|
@@ -1537,10 +1564,14 @@ class GridAdapter {
|
|
|
1537
1564
|
$implicit: ctx.value,
|
|
1538
1565
|
value: ctx.value,
|
|
1539
1566
|
row: ctx.row,
|
|
1567
|
+
field: ctx.field,
|
|
1540
1568
|
column: ctx.column,
|
|
1569
|
+
rowId: ctx.rowId ?? '',
|
|
1541
1570
|
// Preferred: simple callback functions
|
|
1542
1571
|
onCommit,
|
|
1543
1572
|
onCancel,
|
|
1573
|
+
updateRow: ctx.updateRow,
|
|
1574
|
+
onValueChange: ctx.onValueChange,
|
|
1544
1575
|
// FormControl from FormArray (if available)
|
|
1545
1576
|
control,
|
|
1546
1577
|
// Deprecated: EventEmitters (for backwards compatibility)
|
|
@@ -1566,6 +1597,25 @@ class GridAdapter {
|
|
|
1566
1597
|
ctx.cancel();
|
|
1567
1598
|
});
|
|
1568
1599
|
}
|
|
1600
|
+
// Auto-update editor when value changes externally (e.g., via updateRow cascade).
|
|
1601
|
+
// This keeps Angular template editors in sync without manual DOM patching.
|
|
1602
|
+
ctx.onValueChange?.((newVal) => {
|
|
1603
|
+
context.$implicit = newVal;
|
|
1604
|
+
context.value = newVal;
|
|
1605
|
+
viewRef.markForCheck();
|
|
1606
|
+
// Also patch raw DOM inputs as a fallback for editors that don't bind to context
|
|
1607
|
+
if (rootNode) {
|
|
1608
|
+
const input = rootNode.querySelector?.('input,textarea,select');
|
|
1609
|
+
if (input) {
|
|
1610
|
+
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
|
1611
|
+
input.checked = !!newVal;
|
|
1612
|
+
}
|
|
1613
|
+
else {
|
|
1614
|
+
input.value = String(newVal ?? '');
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1569
1619
|
return rootNode;
|
|
1570
1620
|
};
|
|
1571
1621
|
}
|
|
@@ -1734,6 +1784,13 @@ class GridAdapter {
|
|
|
1734
1784
|
// Type assertion needed: adapter bridges TRow to core's unknown
|
|
1735
1785
|
typeDefault.editor = this.createComponentEditor(config.editor);
|
|
1736
1786
|
}
|
|
1787
|
+
// Create filterPanelRenderer function that instantiates the Angular component
|
|
1788
|
+
if (config.filterPanelRenderer && isComponentClass(config.filterPanelRenderer)) {
|
|
1789
|
+
typeDefault.filterPanelRenderer = this.createComponentFilterPanelRenderer(config.filterPanelRenderer);
|
|
1790
|
+
}
|
|
1791
|
+
else if (config.filterPanelRenderer) {
|
|
1792
|
+
typeDefault.filterPanelRenderer = config.filterPanelRenderer;
|
|
1793
|
+
}
|
|
1737
1794
|
return typeDefault;
|
|
1738
1795
|
}
|
|
1739
1796
|
/**
|
|
@@ -1782,12 +1839,32 @@ class GridAdapter {
|
|
|
1782
1839
|
* @internal
|
|
1783
1840
|
*/
|
|
1784
1841
|
createComponentRenderer(componentClass) {
|
|
1842
|
+
// Cell cache for component-based renderers - maps cell element to its component ref
|
|
1843
|
+
const cellCache = new WeakMap();
|
|
1785
1844
|
return (ctx) => {
|
|
1786
|
-
const
|
|
1845
|
+
const cellEl = ctx.cellEl;
|
|
1846
|
+
if (cellEl) {
|
|
1847
|
+
const cached = cellCache.get(cellEl);
|
|
1848
|
+
if (cached) {
|
|
1849
|
+
// Reuse existing component - just update inputs
|
|
1850
|
+
this.setComponentInputs(cached.componentRef, {
|
|
1851
|
+
value: ctx.value,
|
|
1852
|
+
row: ctx.row,
|
|
1853
|
+
column: ctx.column,
|
|
1854
|
+
});
|
|
1855
|
+
cached.componentRef.changeDetectorRef.detectChanges();
|
|
1856
|
+
return cached.hostElement;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const { hostElement, componentRef } = this.mountComponent(componentClass, {
|
|
1787
1860
|
value: ctx.value,
|
|
1788
1861
|
row: ctx.row,
|
|
1789
1862
|
column: ctx.column,
|
|
1790
1863
|
});
|
|
1864
|
+
// Cache for reuse on scroll recycles
|
|
1865
|
+
if (cellEl) {
|
|
1866
|
+
cellCache.set(cellEl, { componentRef, hostElement });
|
|
1867
|
+
}
|
|
1791
1868
|
return hostElement;
|
|
1792
1869
|
};
|
|
1793
1870
|
}
|
|
@@ -1803,9 +1880,57 @@ class GridAdapter {
|
|
|
1803
1880
|
column: ctx.column,
|
|
1804
1881
|
});
|
|
1805
1882
|
this.wireEditorCallbacks(hostElement, componentRef, (value) => ctx.commit(value), () => ctx.cancel());
|
|
1883
|
+
// Auto-update editor when value changes externally (e.g., via updateRow cascade).
|
|
1884
|
+
// This keeps Angular component editors in sync without manual DOM patching.
|
|
1885
|
+
ctx.onValueChange?.((newVal) => {
|
|
1886
|
+
try {
|
|
1887
|
+
componentRef.setInput('value', newVal);
|
|
1888
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
1889
|
+
}
|
|
1890
|
+
catch {
|
|
1891
|
+
// Input doesn't exist or component is destroyed — fall back to DOM patching
|
|
1892
|
+
const input = hostElement.querySelector?.('input,textarea,select');
|
|
1893
|
+
if (input) {
|
|
1894
|
+
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
|
1895
|
+
input.checked = !!newVal;
|
|
1896
|
+
}
|
|
1897
|
+
else {
|
|
1898
|
+
input.value = String(newVal ?? '');
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1806
1903
|
return hostElement;
|
|
1807
1904
|
};
|
|
1808
1905
|
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Creates a filter panel renderer function from an Angular component class.
|
|
1908
|
+
*
|
|
1909
|
+
* The component must implement `FilterPanel` (i.e., have a `params` input).
|
|
1910
|
+
* The component is mounted into the filter panel container element.
|
|
1911
|
+
* @internal
|
|
1912
|
+
*/
|
|
1913
|
+
createComponentFilterPanelRenderer(componentClass) {
|
|
1914
|
+
return (container, params) => {
|
|
1915
|
+
const hostElement = document.createElement('span');
|
|
1916
|
+
hostElement.style.display = 'contents';
|
|
1917
|
+
const componentRef = createComponent(componentClass, {
|
|
1918
|
+
environmentInjector: this.injector,
|
|
1919
|
+
hostElement,
|
|
1920
|
+
});
|
|
1921
|
+
// Set params input
|
|
1922
|
+
try {
|
|
1923
|
+
componentRef.setInput('params', params);
|
|
1924
|
+
}
|
|
1925
|
+
catch {
|
|
1926
|
+
// Input doesn't exist on component — ignore
|
|
1927
|
+
}
|
|
1928
|
+
this.appRef.attachView(componentRef.hostView);
|
|
1929
|
+
this.componentRefs.push(componentRef);
|
|
1930
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
1931
|
+
container.appendChild(hostElement);
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1809
1934
|
/**
|
|
1810
1935
|
* Subscribes to an Angular output on a component instance.
|
|
1811
1936
|
* Works with both EventEmitter and OutputEmitterRef (signal outputs).
|
|
@@ -2366,6 +2491,9 @@ function clearFeatureRegistry() {
|
|
|
2366
2491
|
*/
|
|
2367
2492
|
class BaseGridEditor {
|
|
2368
2493
|
elementRef = inject(ElementRef);
|
|
2494
|
+
_destroyRef = inject(DestroyRef);
|
|
2495
|
+
/** Cleanup function for the edit-close listener */
|
|
2496
|
+
_editCloseCleanup = null;
|
|
2369
2497
|
// ============================================================================
|
|
2370
2498
|
// Inputs
|
|
2371
2499
|
// ============================================================================
|
|
@@ -2461,8 +2589,51 @@ class BaseGridEditor {
|
|
|
2461
2589
|
return Object.entries(ctrl.errors).map(([key, value]) => this.getErrorMessage(key, value));
|
|
2462
2590
|
}, ...(ngDevMode ? [{ debugName: "allErrorMessages" }] : []));
|
|
2463
2591
|
// ============================================================================
|
|
2592
|
+
// Lifecycle
|
|
2593
|
+
// ============================================================================
|
|
2594
|
+
constructor() {
|
|
2595
|
+
afterNextRender(() => this._initEditCloseListener());
|
|
2596
|
+
this._destroyRef.onDestroy(() => {
|
|
2597
|
+
this._editCloseCleanup?.();
|
|
2598
|
+
this._editCloseCleanup = null;
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
_initEditCloseListener() {
|
|
2602
|
+
const grid = this.elementRef.nativeElement.closest('tbw-grid');
|
|
2603
|
+
if (!grid)
|
|
2604
|
+
return;
|
|
2605
|
+
const handler = () => this.onEditClose();
|
|
2606
|
+
grid.addEventListener('edit-close', handler, { once: true });
|
|
2607
|
+
this._editCloseCleanup = () => grid.removeEventListener('edit-close', handler);
|
|
2608
|
+
}
|
|
2609
|
+
// ============================================================================
|
|
2464
2610
|
// Methods
|
|
2465
2611
|
// ============================================================================
|
|
2612
|
+
/**
|
|
2613
|
+
* Whether this editor's cell is the currently focused cell.
|
|
2614
|
+
*
|
|
2615
|
+
* In row editing mode the grid creates editors for every editable cell
|
|
2616
|
+
* in the row simultaneously. Use this to conditionally auto-focus inputs
|
|
2617
|
+
* or open panels only in the active cell.
|
|
2618
|
+
*
|
|
2619
|
+
* Performs a synchronous DOM check — safe to call from `ngAfterViewInit`.
|
|
2620
|
+
*/
|
|
2621
|
+
isCellFocused() {
|
|
2622
|
+
return this.elementRef.nativeElement.closest('[part="cell"]')?.classList.contains('cell-focus') ?? false;
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Called when the grid ends the editing session for this cell.
|
|
2626
|
+
*
|
|
2627
|
+
* Override to perform cleanup such as closing overlay panels, autocomplete
|
|
2628
|
+
* dropdowns, or other floating UI that lives at `<body>` level and would
|
|
2629
|
+
* otherwise persist after the editor DOM is removed.
|
|
2630
|
+
*
|
|
2631
|
+
* The listener is set up automatically via `afterNextRender` — no manual
|
|
2632
|
+
* wiring required.
|
|
2633
|
+
*/
|
|
2634
|
+
onEditClose() {
|
|
2635
|
+
// Default: no-op. Subclasses override.
|
|
2636
|
+
}
|
|
2466
2637
|
/**
|
|
2467
2638
|
* Commit a new value. Emits the commit output AND dispatches a DOM event.
|
|
2468
2639
|
* The DOM event enables the grid's auto-wiring to catch the commit.
|
|
@@ -2526,7 +2697,7 @@ class BaseGridEditor {
|
|
|
2526
2697
|
}
|
|
2527
2698
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: BaseGridEditor, decorators: [{
|
|
2528
2699
|
type: Directive
|
|
2529
|
-
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], commit: [{ type: i0.Output, args: ["commit"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
|
|
2700
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], commit: [{ type: i0.Output, args: ["commit"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
|
|
2530
2701
|
|
|
2531
2702
|
// Symbol for storing form context on the grid element (shared with GridFormArray)
|
|
2532
2703
|
const FORM_ARRAY_CONTEXT = Symbol('formArrayContext');
|
|
@@ -3093,30 +3264,24 @@ class Grid {
|
|
|
3093
3264
|
const existingIcons = angularCfg?.icons || {};
|
|
3094
3265
|
coreConfigOverrides['icons'] = { ...registryIcons, ...existingIcons };
|
|
3095
3266
|
}
|
|
3096
|
-
//
|
|
3097
|
-
const processedConfig = angularCfg ? this.adapter.processGridConfig(angularCfg) : null;
|
|
3098
|
-
// IMPORTANT: If user is NOT using gridConfig input, and there are no feature plugins
|
|
3099
|
-
// or config overrides to merge, do NOT overwrite grid.gridConfig.
|
|
3100
|
-
// This allows [gridConfig]="myConfig" binding to work correctly without the directive
|
|
3101
|
-
// creating a new object that strips properties like typeDefaults.
|
|
3267
|
+
// Nothing to do if there's no config input and no feature inputs
|
|
3102
3268
|
const hasFeaturePlugins = featurePlugins.length > 0;
|
|
3103
3269
|
const hasConfigOverrides = Object.keys(coreConfigOverrides).length > 0;
|
|
3104
|
-
|
|
3105
|
-
const existingConfig = angularCfg || {};
|
|
3106
|
-
if (!processedConfig && !hasFeaturePlugins && !hasConfigOverrides && !angularCfg) {
|
|
3107
|
-
// Nothing to merge and no config input - let the user's DOM binding work directly
|
|
3270
|
+
if (!angularCfg && !hasFeaturePlugins && !hasConfigOverrides) {
|
|
3108
3271
|
return;
|
|
3109
3272
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3273
|
+
const userConfig = angularCfg || {};
|
|
3274
|
+
// Merge feature-input plugins with the user's own plugins
|
|
3275
|
+
const configPlugins = userConfig.plugins || [];
|
|
3112
3276
|
const mergedPlugins = [...featurePlugins, ...configPlugins];
|
|
3113
|
-
//
|
|
3114
|
-
|
|
3277
|
+
// The interceptor on element.gridConfig (installed in ngOnInit)
|
|
3278
|
+
// handles converting component classes → functions via processGridConfig,
|
|
3279
|
+
// so we can pass the raw Angular config through. The interceptor is
|
|
3280
|
+
// idempotent, making this safe even if the config is already processed.
|
|
3115
3281
|
grid.gridConfig = {
|
|
3116
|
-
...
|
|
3117
|
-
...baseConfig, // Then apply processed/angular config
|
|
3282
|
+
...userConfig,
|
|
3118
3283
|
...coreConfigOverrides,
|
|
3119
|
-
plugins: mergedPlugins.length > 0 ? mergedPlugins :
|
|
3284
|
+
plugins: mergedPlugins.length > 0 ? mergedPlugins : userConfig.plugins,
|
|
3120
3285
|
};
|
|
3121
3286
|
});
|
|
3122
3287
|
// Effect to sync loading state to the grid element
|
|
@@ -3921,6 +4086,12 @@ class Grid {
|
|
|
3921
4086
|
this.adapter = new GridAdapter(this.injector, this.appRef, this.viewContainerRef);
|
|
3922
4087
|
DataGridElement.registerAdapter(this.adapter);
|
|
3923
4088
|
const grid = this.elementRef.nativeElement;
|
|
4089
|
+
// Intercept the element's gridConfig setter so that ALL writes
|
|
4090
|
+
// (including Angular's own template property binding when CUSTOM_ELEMENTS_SCHEMA
|
|
4091
|
+
// is used) go through the adapter's processGridConfig first.
|
|
4092
|
+
// This converts Angular component classes to vanilla renderer/editor functions
|
|
4093
|
+
// before the grid's internal ConfigManager ever sees them.
|
|
4094
|
+
this.interceptElementGridConfig(grid);
|
|
3924
4095
|
// Wire up all event listeners based on eventOutputMap
|
|
3925
4096
|
this.setupEventListeners(grid);
|
|
3926
4097
|
// Register adapter on the grid element so MasterDetailPlugin can use it
|
|
@@ -3928,6 +4099,42 @@ class Grid {
|
|
|
3928
4099
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3929
4100
|
grid.__frameworkAdapter = this.adapter;
|
|
3930
4101
|
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Overrides the element's `gridConfig` property so every write is processed
|
|
4104
|
+
* through the adapter before reaching the grid core.
|
|
4105
|
+
*
|
|
4106
|
+
* Why: Angular with `CUSTOM_ELEMENTS_SCHEMA` may bind `[gridConfig]` to both
|
|
4107
|
+
* the directive input AND the native custom-element property. The directive
|
|
4108
|
+
* input feeds an effect that merges feature plugins, but the native property
|
|
4109
|
+
* receives the raw config (with component classes as editors/renderers).
|
|
4110
|
+
* Intercepting the setter guarantees only processed configs reach the grid.
|
|
4111
|
+
*/
|
|
4112
|
+
interceptElementGridConfig(grid) {
|
|
4113
|
+
const proto = Object.getPrototypeOf(grid);
|
|
4114
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'gridConfig');
|
|
4115
|
+
if (!desc?.set || !desc?.get)
|
|
4116
|
+
return;
|
|
4117
|
+
const originalSet = desc.set;
|
|
4118
|
+
const originalGet = desc.get;
|
|
4119
|
+
const adapter = this.adapter;
|
|
4120
|
+
// Instance-level override (does not affect the prototype or other grid elements)
|
|
4121
|
+
Object.defineProperty(grid, 'gridConfig', {
|
|
4122
|
+
get() {
|
|
4123
|
+
return originalGet.call(this);
|
|
4124
|
+
},
|
|
4125
|
+
set(value) {
|
|
4126
|
+
if (value && adapter) {
|
|
4127
|
+
// processGridConfig is idempotent: already-processed functions pass
|
|
4128
|
+
// through isComponentClass unchanged, so double-processing is safe.
|
|
4129
|
+
originalSet.call(this, adapter.processGridConfig(value));
|
|
4130
|
+
}
|
|
4131
|
+
else {
|
|
4132
|
+
originalSet.call(this, value);
|
|
4133
|
+
}
|
|
4134
|
+
},
|
|
4135
|
+
configurable: true,
|
|
4136
|
+
});
|
|
4137
|
+
}
|
|
3931
4138
|
/**
|
|
3932
4139
|
* Sets up event listeners for all outputs using the eventOutputMap.
|
|
3933
4140
|
*/
|
|
@@ -4101,6 +4308,10 @@ class Grid {
|
|
|
4101
4308
|
}
|
|
4102
4309
|
ngOnDestroy() {
|
|
4103
4310
|
const grid = this.elementRef.nativeElement;
|
|
4311
|
+
// Remove the gridConfig interceptor (restores prototype behavior)
|
|
4312
|
+
if (grid) {
|
|
4313
|
+
delete grid['gridConfig'];
|
|
4314
|
+
}
|
|
4104
4315
|
// Cleanup all event listeners
|
|
4105
4316
|
if (grid) {
|
|
4106
4317
|
for (const [eventName, listener] of this.eventListeners) {
|