@toolbox-web/grid-angular 0.13.2 → 0.14.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
@@ -559,7 +559,7 @@ The grid can be used as an Angular form control with `formControlName` or `formC
559
559
  ```typescript
560
560
  import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
561
561
  import { FormControl, ReactiveFormsModule } from '@angular/forms';
562
- import { Grid, GridFormControl } from '@toolbox-web/grid-angular';
562
+ import { Grid, GridFormArray } from '@toolbox-web/grid-angular';
563
563
  import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
564
564
  import type { GridConfig } from '@toolbox-web/grid';
565
565
 
@@ -569,7 +569,7 @@ interface Employee {
569
569
  }
570
570
 
571
571
  @Component({
572
- imports: [Grid, GridFormControl, ReactiveFormsModule],
572
+ imports: [Grid, GridFormArray, ReactiveFormsModule],
573
573
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
574
574
  template: `
575
575
  <tbw-grid [formControl]="employeesControl" [gridConfig]="config" style="height: 400px; display: block;" />
@@ -602,11 +602,11 @@ export class MyComponent {
602
602
  ```typescript
603
603
  import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
604
604
  import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
605
- import { Grid, GridFormControl } from '@toolbox-web/grid-angular';
605
+ import { Grid, GridFormArray } from '@toolbox-web/grid-angular';
606
606
  import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
607
607
 
608
608
  @Component({
609
- imports: [Grid, GridFormControl, ReactiveFormsModule],
609
+ imports: [Grid, GridFormArray, ReactiveFormsModule],
610
610
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
611
611
  template: `
612
612
  <form [formGroup]="form">
@@ -670,7 +670,7 @@ Angular's form system automatically adds these classes to the grid element:
670
670
 
671
671
  Additionally, when the control is disabled:
672
672
 
673
- - `.form-disabled` - Added by `GridFormControl`
673
+ - `.form-disabled` - Added by `GridFormArray`
674
674
 
675
675
  You can style these states:
676
676
 
@@ -692,7 +692,7 @@ For fine-grained control over validation and form state at the cell level, use a
692
692
  ```typescript
693
693
  import { Component, CUSTOM_ELEMENTS_SCHEMA, input, output } from '@angular/core';
694
694
  import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
695
- import { Grid, GridFormControl, TbwEditor, TbwRenderer } from '@toolbox-web/grid-angular';
695
+ import { Grid, GridFormArray, TbwEditor, TbwRenderer } from '@toolbox-web/grid-angular';
696
696
  import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
697
697
 
698
698
  // Custom editor that uses the FormControl directly
@@ -731,7 +731,7 @@ export class ValidatedInputComponent {
731
731
  }
732
732
 
733
733
  @Component({
734
- imports: [Grid, GridFormControl, TbwRenderer, TbwEditor, ReactiveFormsModule, ValidatedInputComponent],
734
+ imports: [Grid, GridFormArray, TbwRenderer, TbwEditor, ReactiveFormsModule, ValidatedInputComponent],
735
735
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
736
736
  template: `
737
737
  <tbw-grid [formControl]="employeesArray" [gridConfig]="config">
@@ -826,12 +826,12 @@ if (context?.hasFormGroups) {
826
826
 
827
827
  The adapter provides base classes that eliminate boilerplate when building custom editors and filter panels.
828
828
 
829
- | Base Class | Extends | Purpose |
830
- | --- | --- | --- |
831
- | `BaseGridEditor` | — | Common inputs (`value`, `row`, `column`, `control`), outputs (`commit`, `cancel`), validation helpers |
832
- | `BaseGridEditorCVA` | `BaseGridEditor` | Adds `ControlValueAccessor` for dual grid + standalone form use |
833
- | `BaseOverlayEditor` | `BaseGridEditor` | Floating overlay panel with CSS Anchor Positioning, focus gating, click-outside detection |
834
- | `BaseFilterPanel` | — | Ready-made `params` input for `FilteringPlugin`, with `applyAndClose()` / `clearAndClose()` helpers |
829
+ | Base Class | Extends | Purpose |
830
+ | ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- |
831
+ | `BaseGridEditor` | — | Common inputs (`value`, `row`, `column`, `control`), outputs (`commit`, `cancel`), validation helpers |
832
+ | `BaseGridEditorCVA` | `BaseGridEditor` | Adds `ControlValueAccessor` for dual grid + standalone form use |
833
+ | `BaseOverlayEditor` | `BaseGridEditor` | Floating overlay panel with CSS Anchor Positioning, focus gating, click-outside detection |
834
+ | `BaseFilterPanel` | — | Ready-made `params` input for `FilteringPlugin`, with `applyAndClose()` / `clearAndClose()` helpers |
835
835
 
836
836
  ### BaseOverlayEditor Example
837
837
 
@@ -853,7 +853,7 @@ import { BaseOverlayEditor } from '@toolbox-web/grid-angular';
853
853
  <input type="date" [value]="currentValue()" (change)="selectAndClose($event.target.value)" />
854
854
  <button (click)="hideOverlay()">Cancel</button>
855
855
  </div>
856
- `
856
+ `,
857
857
  })
858
858
  export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> implements AfterViewInit {
859
859
  @ViewChild('panel') panelRef!: ElementRef<HTMLElement>;
@@ -866,8 +866,12 @@ export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> implem
866
866
  if (this.isCellFocused()) this.showOverlay();
867
867
  }
868
868
 
869
- protected getInlineInput() { return this.inputRef?.nativeElement ?? null; }
870
- protected onOverlayOutsideClick() { this.hideOverlay(); }
869
+ protected getInlineInput() {
870
+ return this.inputRef?.nativeElement ?? null;
871
+ }
872
+ protected onOverlayOutsideClick() {
873
+ this.hideOverlay();
874
+ }
871
875
 
872
876
  selectAndClose(date: string): void {
873
877
  this.commitValue(date);
@@ -888,7 +892,7 @@ import { BaseFilterPanel } from '@toolbox-web/grid-angular';
888
892
  <input #input (keydown.enter)="applyAndClose()" />
889
893
  <button (click)="applyAndClose()">Apply</button>
890
894
  <button (click)="clearAndClose()">Clear</button>
891
- `
895
+ `,
892
896
  })
893
897
  export class TextFilterComponent extends BaseFilterPanel {
894
898
  @ViewChild('input') input!: ElementRef<HTMLInputElement>;
@@ -908,7 +912,7 @@ export class TextFilterComponent extends BaseFilterPanel {
908
912
  | Directive | Selector | Description |
909
913
  | ------------------ | ---------------------------------------------------- | -------------------------------------- |
910
914
  | `Grid` | `tbw-grid` | Main directive, auto-registers adapter |
911
- | `GridFormControl` | `tbw-grid[formControlName]`, `tbw-grid[formControl]` | Reactive Forms integration |
915
+ | `GridFormArray` | `tbw-grid[formControlName]`, `tbw-grid[formControl]` | Reactive Forms integration |
912
916
  | `TbwRenderer` | `*tbwRenderer` | Structural directive for cell views |
913
917
  | `TbwEditor` | `*tbwEditor` | Structural directive for cell editors |
914
918
  | `GridColumnView` | `tbw-grid-column-view` | Nested directive for cell views |
@@ -918,12 +922,12 @@ export class TextFilterComponent extends BaseFilterPanel {
918
922
 
919
923
  ### Base Classes
920
924
 
921
- | Class | Description |
922
- | --- | --- |
923
- | `BaseGridEditor<TRow, TValue>` | Base class for inline cell editors with validation helpers |
925
+ | Class | Description |
926
+ | --------------------------------- | -------------------------------------------------------------------- |
927
+ | `BaseGridEditor<TRow, TValue>` | Base class for inline cell editors with validation helpers |
924
928
  | `BaseGridEditorCVA<TRow, TValue>` | `BaseGridEditor` + `ControlValueAccessor` for dual grid/form editors |
925
- | `BaseOverlayEditor<TRow, TValue>` | `BaseGridEditor` + floating overlay panel infrastructure |
926
- | `BaseFilterPanel` | Base class for custom filter panels with `params` input |
929
+ | `BaseOverlayEditor<TRow, TValue>` | `BaseGridEditor` + floating overlay panel infrastructure |
930
+ | `BaseFilterPanel` | Base class for custom filter panels with `params` input |
927
931
 
928
932
  ### Type Registry
929
933
 
@@ -951,12 +955,29 @@ export class TextFilterComponent extends BaseFilterPanel {
951
955
 
952
956
  ### Grid Directive Outputs
953
957
 
954
- | Output | Type | Description |
955
- | -------------- | --------------------------------- | -------------------- |
956
- | `cellCommit` | `EventEmitter<CellCommitEvent>` | Cell value committed |
957
- | `rowCommit` | `EventEmitter<RowCommitEvent>` | Row edit committed |
958
- | `sortChange` | `EventEmitter<SortChangeEvent>` | Sort state changed |
959
- | `columnResize` | `EventEmitter<ColumnResizeEvent>` | Column resized |
958
+ | Output | Type | Description |
959
+ | ------------------- | ------------------------------------------ | ---------------------------- |
960
+ | `cellCommit` | `OutputEmitterRef<CellCommitEvent>` | Cell value committed |
961
+ | `rowCommit` | `OutputEmitterRef<RowCommitEvent>` | Row edit committed |
962
+ | `sortChange` | `OutputEmitterRef<SortChangeDetail>` | Sort state changed |
963
+ | `columnResize` | `OutputEmitterRef<ColumnResizeDetail>` | Column resized |
964
+ | `cellClick` | `OutputEmitterRef<CellClickDetail>` | Cell clicked |
965
+ | `rowClick` | `OutputEmitterRef<RowClickDetail>` | Row clicked |
966
+ | `cellActivate` | `OutputEmitterRef<CellActivateDetail>` | Cell focus changed |
967
+ | `cellChange` | `OutputEmitterRef<CellChangeDetail>` | Cell value changed |
968
+ | `changedRowsReset` | `OutputEmitterRef<ChangedRowsResetDetail>` | Changed rows cache cleared |
969
+ | `filterChange` | `OutputEmitterRef<FilterChangeDetail>` | Filter state changed |
970
+ | `columnMove` | `OutputEmitterRef<ColumnMoveDetail>` | Column moved |
971
+ | `columnVisibility` | `OutputEmitterRef<ColumnVisibilityDetail>` | Column visibility changed |
972
+ | `columnStateChange` | `OutputEmitterRef<GridColumnState>` | Column state changed |
973
+ | `selectionChange` | `OutputEmitterRef<SelectionChangeDetail>` | Selection changed |
974
+ | `rowMove` | `OutputEmitterRef<RowMoveDetail>` | Row moved (drag & drop) |
975
+ | `groupToggle` | `OutputEmitterRef<GroupToggleDetail>` | Group expanded/collapsed |
976
+ | `treeExpand` | `OutputEmitterRef<TreeExpandDetail>` | Tree node expanded/collapsed |
977
+ | `detailExpand` | `OutputEmitterRef<DetailExpandDetail>` | Detail panel toggled |
978
+ | `responsiveChange` | `OutputEmitterRef<ResponsiveChangeDetail>` | Responsive mode changed |
979
+ | `copy` | `OutputEmitterRef<CopyDetail>` | Data copied to clipboard |
980
+ | `paste` | `OutputEmitterRef<PasteDetail>` | Data pasted from clipboard |
960
981
 
961
982
  ### GridDetailView Inputs
962
983
 
@@ -1008,12 +1029,7 @@ import type {
1008
1029
  } from '@toolbox-web/grid-angular';
1009
1030
 
1010
1031
  // Base classes for custom editors and filter panels
1011
- import {
1012
- BaseGridEditor,
1013
- BaseGridEditorCVA,
1014
- BaseOverlayEditor,
1015
- BaseFilterPanel,
1016
- } from '@toolbox-web/grid-angular';
1032
+ import { BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, BaseFilterPanel } from '@toolbox-web/grid-angular';
1017
1033
 
1018
1034
  // Type guard for component class detection
1019
1035
  import { isComponentClass } from '@toolbox-web/grid-angular';
@@ -90,6 +90,7 @@ function injectGridSelection() {
90
90
  // Reactive selection state
91
91
  const selectionSignal = signal(null, ...(ngDevMode ? [{ debugName: "selectionSignal" }] : []));
92
92
  const selectedRowIndicesSignal = signal([], ...(ngDevMode ? [{ debugName: "selectedRowIndicesSignal" }] : []));
93
+ const selectedRowsSignal = signal([], ...(ngDevMode ? [{ debugName: "selectedRowsSignal" }] : []));
93
94
  // Lazy discovery: cached grid reference
94
95
  let cachedGrid = null;
95
96
  let readyPromiseStarted = false;
@@ -104,6 +105,7 @@ function injectGridSelection() {
104
105
  if (plugin) {
105
106
  selectionSignal.set(plugin.getSelection());
106
107
  selectedRowIndicesSignal.set(detail.mode === 'row' ? plugin.getSelectedRowIndices() : []);
108
+ selectedRowsSignal.set(plugin.getSelectedRows());
107
109
  }
108
110
  };
109
111
  /**
@@ -151,6 +153,7 @@ function injectGridSelection() {
151
153
  selectionSignal.set(plugin.getSelection());
152
154
  const mode = plugin.config?.mode;
153
155
  selectedRowIndicesSignal.set(mode === 'row' ? plugin.getSelectedRowIndices() : []);
156
+ selectedRowsSignal.set(plugin.getSelectedRows());
154
157
  }
155
158
  };
156
159
  // Discover the grid after the first render so the selection-change
@@ -179,6 +182,7 @@ function injectGridSelection() {
179
182
  isReady: isReady.asReadonly(),
180
183
  selection: selectionSignal.asReadonly(),
181
184
  selectedRowIndices: selectedRowIndicesSignal.asReadonly(),
185
+ selectedRows: selectedRowsSignal.asReadonly(),
182
186
  selectAll: () => {
183
187
  const plugin = getPlugin();
184
188
  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 { afterNextRender, 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 /**\n * Sync reactive signals with the current plugin state.\n * Called once when the grid is first discovered and ready.\n */\n const syncSignals = (): void => {\n const plugin = getPlugin();\n if (plugin) {\n selectionSignal.set(plugin.getSelection());\n const mode = (plugin as any).config?.mode;\n selectedRowIndicesSignal.set(mode === 'row' ? plugin.getSelectedRowIndices() : []);\n }\n };\n\n // Discover the grid after the first render so the selection-change\n // listener is attached without requiring a programmatic method call.\n // Uses a MutationObserver as fallback for lazy-rendered tabs, *ngIf,\n // @defer, etc. where the grid may not be in the DOM on first render.\n afterNextRender(() => {\n const grid = getGrid();\n if (grid) {\n grid.ready?.().then(syncSignals);\n return;\n }\n\n // Grid not in DOM yet — watch for it to appear.\n const host = elementRef.nativeElement as HTMLElement;\n const observer = new MutationObserver(() => {\n const discovered = getGrid();\n if (discovered) {\n observer.disconnect();\n discovered.ready?.().then(syncSignals);\n }\n });\n observer.observe(host, { childList: true, subtree: true });\n\n destroyRef.onDestroy(() => observer.disconnect());\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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"}
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 { afterNextRender, 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<TRow = unknown> {\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 * **Prefer `selectedRows`** for getting actual row objects — it handles\n * index-to-object resolution correctly regardless of sorting/filtering.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selectedRowIndices().length }} rows selected\n * ```\n */\n selectedRowIndices: Signal<number[]>;\n\n /**\n * Reactive selected row objects. Updates automatically whenever the selection changes.\n * Works in all selection modes (row, cell, range) — returns the actual row objects\n * from the grid's processed (sorted/filtered) rows.\n *\n * This is the recommended way to get selected rows. Unlike manual index mapping,\n * it correctly resolves rows even when the grid is sorted or filtered.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection<Employee>();\n *\n * // In template:\n * // {{ selection.selectedRows().length }} rows selected\n *\n * // In computed:\n * readonly hasSelection = computed(() => this.selection.selectedRows().length > 0);\n * ```\n */\n selectedRows: Signal<TRow[]>;\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<TRow> {\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 const selectedRowsSignal = signal<TRow[]>([]);\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 selectedRowsSignal.set(plugin.getSelectedRows<TRow>());\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 /**\n * Sync reactive signals with the current plugin state.\n * Called once when the grid is first discovered and ready.\n */\n const syncSignals = (): void => {\n const plugin = getPlugin();\n if (plugin) {\n selectionSignal.set(plugin.getSelection());\n const mode = (plugin as any).config?.mode;\n selectedRowIndicesSignal.set(mode === 'row' ? plugin.getSelectedRowIndices() : []);\n selectedRowsSignal.set(plugin.getSelectedRows<TRow>());\n }\n };\n\n // Discover the grid after the first render so the selection-change\n // listener is attached without requiring a programmatic method call.\n // Uses a MutationObserver as fallback for lazy-rendered tabs, *ngIf,\n // @defer, etc. where the grid may not be in the DOM on first render.\n afterNextRender(() => {\n const grid = getGrid();\n if (grid) {\n grid.ready?.().then(syncSignals);\n return;\n }\n\n // Grid not in DOM yet — watch for it to appear.\n const host = elementRef.nativeElement as HTMLElement;\n const observer = new MutationObserver(() => {\n const discovered = getGrid();\n if (discovered) {\n observer.disconnect();\n discovered.ready?.().then(syncSignals);\n }\n });\n observer.observe(host, { childList: true, subtree: true });\n\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n return {\n isReady: isReady.asReadonly(),\n selection: selectionSignal.asReadonly(),\n selectedRowIndices: selectedRowIndicesSignal.asReadonly(),\n selectedRows: selectedRowsSignal.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":";;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCG;AAYH,eAAe,CAAC,WAAW,EAAE,CAAC,MAAM,KAAI;;AAEtC,IAAA,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,OAAO,EAAE;QAC/D,OAAO,IAAI,eAAe,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC9C;;AAEA,IAAA,OAAO,IAAI,eAAe,CAAC,MAAM,IAAI,SAAS,CAAC;AACjD,CAAC,CAAC;AAkGF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCG;SACa,mBAAmB,GAAA;AACjC,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,mDAAC;;AAG7B,IAAA,MAAM,eAAe,GAAG,MAAM,CAAyB,IAAI,2DAAC;AAC5D,IAAA,MAAM,wBAAwB,GAAG,MAAM,CAAW,EAAE,oEAAC;AACrD,IAAA,MAAM,kBAAkB,GAAG,MAAM,CAAS,EAAE,8DAAC;;IAG7C,IAAI,UAAU,GAAiC,IAAI;IACnD,IAAI,mBAAmB,GAAG,KAAK;IAC/B,IAAI,gBAAgB,GAAG,KAAK;AAE5B;;;AAGG;AACH,IAAA,MAAM,iBAAiB,GAAG,CAAC,CAAQ,KAAU;AAC3C,QAAA,MAAM,MAAM,GAAI,CAAwC,CAAC,MAAM;AAC/D,QAAA,MAAM,MAAM,GAAG,SAAS,EAAE;QAC1B,IAAI,MAAM,EAAE;YACV,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC1C,wBAAwB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,GAAG,MAAM,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC;YACzF,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,EAAQ,CAAC;QACxD;AACF,IAAA,CAAC;AAED;;;AAGG;AACH,IAAA,MAAM,cAAc,GAAG,CAAC,IAA2B,KAAU;AAC3D,QAAA,IAAI,gBAAgB;YAAE;QACtB,gBAAgB,GAAG,IAAI;AAEvB,QAAA,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,iBAAiB,CAAC;AAE5D,QAAA,UAAU,CAAC,SAAS,CAAC,MAAK;AACxB,YAAA,IAAI,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,iBAAiB,CAAC;AACjE,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC;AAED;;;AAGG;IACH,MAAM,OAAO,GAAG,MAAmC;AACjD,QAAA,IAAI,UAAU;AAAE,YAAA,OAAO,UAAU;QAEjC,MAAM,IAAI,GAAG,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,UAAU,CAAiC;QAC/F,IAAI,IAAI,EAAE;YACR,UAAU,GAAG,IAAI;YACjB,cAAc,CAAC,IAAI,CAAC;;YAEpB,IAAI,CAAC,mBAAmB,EAAE;gBACxB,mBAAmB,GAAG,IAAI;AAC1B,gBAAA,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C;QACF;AACA,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;IAED,MAAM,SAAS,GAAG,MAAkC;AAClD,QAAA,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC,eAAe,CAAC;AAC9C,IAAA,CAAC;AAED;;;AAGG;IACH,MAAM,WAAW,GAAG,MAAW;AAC7B,QAAA,MAAM,MAAM,GAAG,SAAS,EAAE;QAC1B,IAAI,MAAM,EAAE;YACV,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;AAC1C,YAAA,MAAM,IAAI,GAAI,MAAc,CAAC,MAAM,EAAE,IAAI;AACzC,YAAA,wBAAwB,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,GAAG,MAAM,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC;YAClF,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,EAAQ,CAAC;QACxD;AACF,IAAA,CAAC;;;;;IAMD,eAAe,CAAC,MAAK;AACnB,QAAA,MAAM,IAAI,GAAG,OAAO,EAAE;QACtB,IAAI,IAAI,EAAE;YACR,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;YAChC;QACF;;AAGA,QAAA,MAAM,IAAI,GAAG,UAAU,CAAC,aAA4B;AACpD,QAAA,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CAAC,MAAK;AACzC,YAAA,MAAM,UAAU,GAAG,OAAO,EAAE;YAC5B,IAAI,UAAU,EAAE;gBACd,QAAQ,CAAC,UAAU,EAAE;gBACrB,UAAU,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;YACxC;AACF,QAAA,CAAC,CAAC;AACF,QAAA,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAE1D,UAAU,CAAC,SAAS,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;AACnD,IAAA,CAAC,CAAC;IAEF,OAAO;AACL,QAAA,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE;AAC7B,QAAA,SAAS,EAAE,eAAe,CAAC,UAAU,EAAE;AACvC,QAAA,kBAAkB,EAAE,wBAAwB,CAAC,UAAU,EAAE;AACzD,QAAA,YAAY,EAAE,kBAAkB,CAAC,UAAU,EAAE;QAE7C,SAAS,EAAE,MAAK;AACd,YAAA,MAAM,MAAM,GAAG,SAAS,EAAE;YAC1B,IAAI,CAAC,MAAM,EAAE;gBACX,OAAO,CAAC,IAAI,CACV,CAAA,mDAAA,CAAqD;oBACnD,CAAA,mCAAA,CAAqC;AACrC,oBAAA,CAAA,sCAAA,CAAwC,CAC3C;gBACD;YACF;AACA,YAAA,MAAM,IAAI,GAAG,OAAO,EAAE;;AAEtB,YAAA,MAAM,IAAI,GAAI,MAAc,CAAC,MAAM,EAAE,IAAI;AAEzC,YAAA,IAAI,IAAI,KAAK,KAAK,EAAE;gBAClB,MAAM,QAAQ,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AACxC,gBAAA,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU;gBACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE;AAAE,oBAAA,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACnD,gBAAA,MAAc,CAAC,QAAQ,GAAG,UAAU;AACpC,gBAAA,MAAc,CAAC,kBAAkB,IAAI;YACxC;AAAO,iBAAA,IAAI,IAAI,KAAK,OAAO,EAAE;gBAC3B,MAAM,QAAQ,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;gBACxC,MAAM,QAAQ,GAAI,IAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;gBACrD,IAAI,QAAQ,GAAG,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE;AAChC,oBAAA,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;gBAChG;YACF;QACF,CAAC;QAED,cAAc,EAAE,MAAK;AACnB,YAAA,SAAS,EAAE,EAAE,cAAc,EAAE;QAC/B,CAAC;QAED,YAAY,EAAE,MAAK;AACjB,YAAA,OAAO,SAAS,EAAE,EAAE,YAAY,EAAE,IAAI,IAAI;QAC5C,CAAC;AAED,QAAA,cAAc,EAAE,CAAC,GAAW,EAAE,GAAW,KAAI;YAC3C,OAAO,SAAS,EAAE,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,KAAK;QACvD,CAAC;AAED,QAAA,SAAS,EAAE,CAAC,MAAmB,KAAI;AACjC,YAAA,SAAS,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC;QAChC,CAAC;KACF;AACH;;AC3VA;;AAEG;;;;"}
@@ -1141,6 +1141,88 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
1141
1141
  args: [{ selector: '[tbwEditor]' }]
1142
1142
  }], ctorParameters: () => [] });
1143
1143
 
1144
+ /**
1145
+ * Editor Wiring Helpers
1146
+ *
1147
+ * Pure functions for wiring up commit/cancel handlers on editor components.
1148
+ * Extracted from GridAdapter to enable unit testing without Angular DI.
1149
+ *
1150
+ * @internal
1151
+ */
1152
+ // #region subscribeToOutput
1153
+ /**
1154
+ * Subscribes to an Angular output on a component instance.
1155
+ * Works with both EventEmitter and OutputEmitterRef (signal outputs).
1156
+ *
1157
+ * @param instance - The component instance (as a plain record)
1158
+ * @param outputName - Name of the output property
1159
+ * @param callback - Callback to invoke when the output emits
1160
+ * @returns `true` if the output was found and subscribed, `false` otherwise
1161
+ * @internal
1162
+ */
1163
+ function subscribeToOutput(instance, outputName, callback) {
1164
+ const output = instance[outputName];
1165
+ if (!output)
1166
+ return false;
1167
+ // Check if it's an Observable-like (EventEmitter or OutputEmitterRef)
1168
+ if (typeof output.subscribe === 'function') {
1169
+ output.subscribe(callback);
1170
+ return true;
1171
+ }
1172
+ return false;
1173
+ }
1174
+ // #endregion
1175
+ // #region wireEditorCallbacks
1176
+ /**
1177
+ * Wire up commit/cancel handlers for an editor component.
1178
+ *
1179
+ * Supports both Angular outputs and DOM CustomEvents. When both fire
1180
+ * (the BaseGridEditor pattern), a per-action flag prevents the callback
1181
+ * from running twice.
1182
+ *
1183
+ * @param hostElement - The host DOM element for the editor
1184
+ * @param instance - The component instance (as a plain record)
1185
+ * @param commit - Callback to invoke when committing a value
1186
+ * @param cancel - Callback to invoke when cancelling the edit
1187
+ * @internal
1188
+ */
1189
+ function wireEditorCallbacks(hostElement, instance, commit, cancel) {
1190
+ // Guard: when both Angular output AND DOM event fire (BaseGridEditor.commitValue
1191
+ // emits both), only the first should call commit/cancel(). The flags prevent
1192
+ // double-fires that cause redundant cell-commit events and extra dirty-tracking work.
1193
+ let commitHandledByOutput = false;
1194
+ let cancelHandledByOutput = false;
1195
+ subscribeToOutput(instance, 'commit', (value) => {
1196
+ commitHandledByOutput = true;
1197
+ commit(value);
1198
+ });
1199
+ subscribeToOutput(instance, 'cancel', () => {
1200
+ cancelHandledByOutput = true;
1201
+ cancel();
1202
+ });
1203
+ // Also listen for DOM CustomEvents as a fallback for editors that don't
1204
+ // have Angular commit/cancel outputs (e.g., third-party web components).
1205
+ hostElement.addEventListener('commit', (e) => {
1206
+ e.stopPropagation();
1207
+ if (commitHandledByOutput) {
1208
+ // Already handled by the Angular output subscription — reset and skip.
1209
+ commitHandledByOutput = false;
1210
+ return;
1211
+ }
1212
+ const customEvent = e;
1213
+ commit(customEvent.detail);
1214
+ });
1215
+ hostElement.addEventListener('cancel', (e) => {
1216
+ e.stopPropagation();
1217
+ if (cancelHandledByOutput) {
1218
+ cancelHandledByOutput = false;
1219
+ return;
1220
+ }
1221
+ cancel();
1222
+ });
1223
+ }
1224
+ // #endregion
1225
+
1144
1226
  /**
1145
1227
  * Type-level default registry for Angular applications.
1146
1228
  *
@@ -1363,6 +1445,32 @@ function getAnyEditorTemplate(element) {
1363
1445
  * - Handles editor callbacks (onCommit/onCancel)
1364
1446
  * - Manages view lifecycle and change detection
1365
1447
  */
1448
+ /**
1449
+ * Synchronize an embedded view's rootNodes into a stable container element.
1450
+ *
1451
+ * Angular's control flow blocks (@if, @for, @switch) can dynamically add or
1452
+ * remove rootNodes during `detectChanges()`. This helper ensures the container
1453
+ * always reflects the current set of rootNodes, preventing orphaned or stale
1454
+ * nodes when the template's DOM structure changes between renders.
1455
+ */
1456
+ function syncRootNodes(viewRef, container) {
1457
+ // Fast path: if the container already holds exactly the right nodes, skip DOM mutations.
1458
+ const rootNodes = viewRef.rootNodes;
1459
+ const children = container.childNodes;
1460
+ let needsSync = children.length !== rootNodes.length;
1461
+ if (!needsSync) {
1462
+ for (let i = 0; i < rootNodes.length; i++) {
1463
+ if (children[i] !== rootNodes[i]) {
1464
+ needsSync = true;
1465
+ break;
1466
+ }
1467
+ }
1468
+ }
1469
+ if (needsSync) {
1470
+ // Clear and re-append. replaceChildren is efficient (single reflow).
1471
+ container.replaceChildren(...rootNodes);
1472
+ }
1473
+ }
1366
1474
  class GridAdapter {
1367
1475
  injector;
1368
1476
  appRef;
@@ -1494,11 +1602,17 @@ class GridAdapter {
1494
1602
  // This is important when only an editor template is provided (no view template)
1495
1603
  return undefined;
1496
1604
  }
1497
- // Cell cache for this column - maps cell element to its view ref and root node.
1605
+ // Cell cache for this column - maps cell element to its view ref and container.
1498
1606
  // When the grid recycles pool elements during scroll, the same cellEl is reused
1499
1607
  // for different row data. By caching per cellEl, we reuse the Angular view and
1500
1608
  // just update its context instead of creating a new embedded view every time.
1501
1609
  // This matches what React and Vue adapters do with their cell caches.
1610
+ //
1611
+ // IMPORTANT: We always use a stable wrapper container (display:contents) rather
1612
+ // than caching individual rootNodes. This is critical because Angular's control
1613
+ // flow (@if, @for, @switch) can dynamically add/remove rootNodes during
1614
+ // detectChanges(). If we cached a single rootNode, newly created nodes (e.g.,
1615
+ // from an @if becoming true) would be orphaned outside the grid cell.
1502
1616
  const cellCache = new WeakMap();
1503
1617
  return (ctx) => {
1504
1618
  // Skip rendering if the cell is in editing mode
@@ -1516,7 +1630,10 @@ class GridAdapter {
1516
1630
  cached.viewRef.context.row = ctx.row;
1517
1631
  cached.viewRef.context.column = ctx.column;
1518
1632
  cached.viewRef.detectChanges();
1519
- return cached.rootNode;
1633
+ // Re-sync rootNodes into the container. Angular's control flow (@if/@for)
1634
+ // may have added or removed nodes during detectChanges().
1635
+ syncRootNodes(cached.viewRef, cached.container);
1636
+ return cached.container;
1520
1637
  }
1521
1638
  }
1522
1639
  // Create the context for the template
@@ -1531,29 +1648,16 @@ class GridAdapter {
1531
1648
  this.viewRefs.push(viewRef);
1532
1649
  // Trigger change detection
1533
1650
  viewRef.detectChanges();
1534
- // Find the first Element root node. When *tbwRenderer is used on <ng-container>,
1535
- // rootNodes[0] is a comment node (<!--ng-container-->); the actual content is in
1536
- // subsequent root nodes. For single-element templates, rootNodes[0] IS the element.
1537
- let rootNode = viewRef.rootNodes[0];
1538
- const elementNodes = viewRef.rootNodes.filter((n) => n.nodeType === Node.ELEMENT_NODE);
1539
- if (elementNodes.length === 1) {
1540
- // Single element among the root nodes — use it directly
1541
- rootNode = elementNodes[0];
1542
- }
1543
- else if (elementNodes.length > 1) {
1544
- // Multiple element nodes — wrap in a span container so all are rendered
1545
- const wrapper = document.createElement('span');
1546
- wrapper.style.display = 'contents';
1547
- for (const node of viewRef.rootNodes) {
1548
- wrapper.appendChild(node);
1549
- }
1550
- rootNode = wrapper;
1551
- }
1651
+ // Always use a stable wrapper container so Angular can freely add/remove
1652
+ // rootNodes (via @if, @for, etc.) without orphaning them outside the grid cell.
1653
+ const container = document.createElement('span');
1654
+ container.style.display = 'contents';
1655
+ syncRootNodes(viewRef, container);
1552
1656
  // Cache for reuse on scroll recycles
1553
1657
  if (cellEl) {
1554
- cellCache.set(cellEl, { viewRef, rootNode });
1658
+ cellCache.set(cellEl, { viewRef, container });
1555
1659
  }
1556
- return rootNode;
1660
+ return container;
1557
1661
  };
1558
1662
  }
1559
1663
  /**
@@ -1872,24 +1976,6 @@ class GridAdapter {
1872
1976
  componentRef.changeDetectorRef.detectChanges();
1873
1977
  return { hostElement, componentRef };
1874
1978
  }
1875
- /**
1876
- * Wires up commit/cancel handlers for an editor component.
1877
- * Supports both Angular outputs and DOM CustomEvents.
1878
- * @internal
1879
- */
1880
- wireEditorCallbacks(hostElement, componentRef, commit, cancel) {
1881
- // Subscribe to Angular outputs (commit/cancel) on the component instance.
1882
- // This works with Angular's output() signal API.
1883
- const instance = componentRef.instance;
1884
- this.subscribeToOutput(instance, 'commit', commit);
1885
- this.subscribeToOutput(instance, 'cancel', cancel);
1886
- // Also listen for DOM events as fallback (for components that dispatch CustomEvents)
1887
- hostElement.addEventListener('commit', (e) => {
1888
- const customEvent = e;
1889
- commit(customEvent.detail);
1890
- });
1891
- hostElement.addEventListener('cancel', () => cancel());
1892
- }
1893
1979
  /**
1894
1980
  * Creates a renderer function from an Angular component class.
1895
1981
  * @internal
@@ -1935,7 +2021,7 @@ class GridAdapter {
1935
2021
  row: ctx.row,
1936
2022
  column: ctx.column,
1937
2023
  }, true);
1938
- this.wireEditorCallbacks(hostElement, componentRef, (value) => ctx.commit(value), () => ctx.cancel());
2024
+ wireEditorCallbacks(hostElement, componentRef.instance, (value) => ctx.commit(value), () => ctx.cancel());
1939
2025
  // Auto-update editor when value changes externally (e.g., via updateRow cascade).
1940
2026
  // This keeps Angular component editors in sync without manual DOM patching.
1941
2027
  ctx.onValueChange?.((newVal) => {
@@ -1987,20 +2073,6 @@ class GridAdapter {
1987
2073
  container.appendChild(hostElement);
1988
2074
  };
1989
2075
  }
1990
- /**
1991
- * Subscribes to an Angular output on a component instance.
1992
- * Works with both EventEmitter and OutputEmitterRef (signal outputs).
1993
- * @internal
1994
- */
1995
- subscribeToOutput(instance, outputName, callback) {
1996
- const output = instance[outputName];
1997
- if (!output)
1998
- return;
1999
- // Check if it's an Observable-like (EventEmitter or OutputEmitterRef)
2000
- if (typeof output.subscribe === 'function') {
2001
- output.subscribe(callback);
2002
- }
2003
- }
2004
2076
  /**
2005
2077
  * Sets component inputs using Angular's setInput API.
2006
2078
  * @internal