@toolbox-web/grid-angular 0.13.2 → 0.13.3

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';
@@ -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
  *
@@ -1872,24 +1954,6 @@ class GridAdapter {
1872
1954
  componentRef.changeDetectorRef.detectChanges();
1873
1955
  return { hostElement, componentRef };
1874
1956
  }
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
1957
  /**
1894
1958
  * Creates a renderer function from an Angular component class.
1895
1959
  * @internal
@@ -1935,7 +1999,7 @@ class GridAdapter {
1935
1999
  row: ctx.row,
1936
2000
  column: ctx.column,
1937
2001
  }, true);
1938
- this.wireEditorCallbacks(hostElement, componentRef, (value) => ctx.commit(value), () => ctx.cancel());
2002
+ wireEditorCallbacks(hostElement, componentRef.instance, (value) => ctx.commit(value), () => ctx.cancel());
1939
2003
  // Auto-update editor when value changes externally (e.g., via updateRow cascade).
1940
2004
  // This keeps Angular component editors in sync without manual DOM patching.
1941
2005
  ctx.onValueChange?.((newVal) => {
@@ -1987,20 +2051,6 @@ class GridAdapter {
1987
2051
  container.appendChild(hostElement);
1988
2052
  };
1989
2053
  }
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
2054
  /**
2005
2055
  * Sets component inputs using Angular's setInput API.
2006
2056
  * @internal