@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.
@@ -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 { hostElement } = this.mountComponent(componentClass, {
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
- // If gridConfig is provided, process it (converts component classes to renderer functions)
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
- // The input signal gives us reactive tracking of the user's config
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
- // Merge: processed config < feature plugins
3111
- const configPlugins = processedConfig?.plugins || existingConfig.plugins || [];
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
- // Build the final config, preserving ALL existing properties (including typeDefaults)
3114
- const baseConfig = processedConfig || existingConfig;
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
- ...existingConfig, // Start with existing config to preserve all properties (including typeDefaults)
3117
- ...baseConfig, // Then apply processed/angular config
3282
+ ...userConfig,
3118
3283
  ...coreConfigOverrides,
3119
- plugins: mergedPlugins.length > 0 ? mergedPlugins : baseConfig.plugins,
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) {