@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 +51 -35
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs +4 -0
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular.mjs +127 -55
- 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 +24 -6
- package/types/toolbox-web-grid-angular-features-selection.d.ts.map +1 -1
- package/types/toolbox-web-grid-angular.d.ts +0 -72
- package/types/toolbox-web-grid-angular.d.ts.map +1 -1
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 `
|
|
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,
|
|
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,
|
|
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
|
|
830
|
-
|
|
|
831
|
-
| `BaseGridEditor`
|
|
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`
|
|
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() {
|
|
870
|
-
|
|
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
|
-
| `
|
|
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
|
|
922
|
-
|
|
|
923
|
-
| `BaseGridEditor<TRow, TValue>`
|
|
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`
|
|
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
|
|
955
|
-
|
|
|
956
|
-
| `cellCommit`
|
|
957
|
-
| `rowCommit`
|
|
958
|
-
| `sortChange`
|
|
959
|
-
| `columnResize`
|
|
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":"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"}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
1535
|
-
// rootNodes
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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,
|
|
1658
|
+
cellCache.set(cellEl, { viewRef, container });
|
|
1555
1659
|
}
|
|
1556
|
-
return
|
|
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
|
-
|
|
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
|