@toolbox-web/grid-angular 0.17.0 → 0.18.1

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.
@@ -356,9 +356,9 @@ function getFormArrayContext(gridElement) {
356
356
  class GridFormArray {
357
357
  destroyRef = inject(DestroyRef);
358
358
  elementRef = inject((ElementRef));
359
- cellCommitListener = null;
360
- cellCancelListener = null;
361
- rowCommitListener = null;
359
+ cellCommitUnsub = null;
360
+ cellCancelUnsub = null;
361
+ rowCommitUnsub = null;
362
362
  touchListener = null;
363
363
  valueChangesSubscription = null;
364
364
  statusChangesSubscriptions = [];
@@ -416,25 +416,19 @@ class GridFormArray {
416
416
  // Store the form context on the grid element for other directives to access
417
417
  this.#storeFormContext(grid);
418
418
  // Intercept cell-commit events to update the FormArray
419
- this.cellCommitListener = (e) => {
420
- const detail = e.detail;
419
+ this.cellCommitUnsub = grid.on('cell-commit', (detail) => {
421
420
  this.#handleCellCommit(detail);
422
- };
423
- grid.addEventListener('cell-commit', this.cellCommitListener);
421
+ });
424
422
  // Intercept cell-cancel events to revert FormControls (grid mode Escape)
425
- this.cellCancelListener = (e) => {
426
- const detail = e.detail;
423
+ this.cellCancelUnsub = grid.on('cell-cancel', (detail) => {
427
424
  this.#handleCellCancel(detail);
428
- };
429
- grid.addEventListener('cell-cancel', this.cellCancelListener);
425
+ });
430
426
  // Intercept row-commit events to prevent if FormGroup is invalid
431
- this.rowCommitListener = (e) => {
427
+ this.rowCommitUnsub = grid.on('row-commit', (detail, event) => {
432
428
  if (!this.syncValidation())
433
429
  return;
434
- const detail = e.detail;
435
- this.#handleRowCommit(e, detail);
436
- };
437
- grid.addEventListener('row-commit', this.rowCommitListener);
430
+ this.#handleRowCommit(event, detail);
431
+ });
438
432
  // Mark FormArray as touched on first interaction
439
433
  this.touchListener = () => {
440
434
  this.formArray().markAsTouched();
@@ -459,15 +453,9 @@ class GridFormArray {
459
453
  const grid = this.elementRef.nativeElement;
460
454
  if (!grid)
461
455
  return;
462
- if (this.cellCommitListener) {
463
- grid.removeEventListener('cell-commit', this.cellCommitListener);
464
- }
465
- if (this.cellCancelListener) {
466
- grid.removeEventListener('cell-cancel', this.cellCancelListener);
467
- }
468
- if (this.rowCommitListener) {
469
- grid.removeEventListener('row-commit', this.rowCommitListener);
470
- }
456
+ this.cellCommitUnsub?.();
457
+ this.cellCancelUnsub?.();
458
+ this.rowCommitUnsub?.();
471
459
  if (this.touchListener) {
472
460
  grid.removeEventListener('click', this.touchListener);
473
461
  }
@@ -1566,14 +1554,28 @@ class GridAdapter {
1566
1554
  * @returns Processed GridConfig with actual renderer/editor functions
1567
1555
  */
1568
1556
  processGridConfig(config) {
1569
- const result = { ...config };
1557
+ return this.processConfig(config);
1558
+ }
1559
+ /**
1560
+ * FrameworkAdapter.processConfig implementation.
1561
+ * Called automatically by the grid's `set gridConfig` setter.
1562
+ */
1563
+ processConfig(config) {
1564
+ // Cast to Angular's extended GridConfig since the config may contain
1565
+ // Angular component classes as renderers/editors at runtime
1566
+ const angularConfig = config;
1567
+ const result = { ...angularConfig };
1570
1568
  // Process columns
1571
- if (config.columns) {
1572
- result.columns = config.columns.map((col) => this.processColumn(col));
1569
+ if (angularConfig.columns) {
1570
+ result.columns = angularConfig.columns.map((col) => this.processColumn(col));
1573
1571
  }
1574
1572
  // Process typeDefaults - convert Angular component classes to renderer/editor functions
1575
- if (config.typeDefaults) {
1576
- result.typeDefaults = this.processTypeDefaults(config.typeDefaults);
1573
+ if (angularConfig.typeDefaults) {
1574
+ result.typeDefaults = this.processTypeDefaults(angularConfig.typeDefaults);
1575
+ }
1576
+ // Process loadingRenderer - convert Angular component class to function
1577
+ if (angularConfig.loadingRenderer && isComponentClass(angularConfig.loadingRenderer)) {
1578
+ result.loadingRenderer = this.createComponentLoadingRenderer(angularConfig.loadingRenderer);
1577
1579
  }
1578
1580
  return result;
1579
1581
  }
@@ -1621,6 +1623,14 @@ class GridAdapter {
1621
1623
  if (column.editor && isComponentClass(column.editor)) {
1622
1624
  processed.editor = this.createComponentEditor(column.editor);
1623
1625
  }
1626
+ // Convert headerRenderer component class to function
1627
+ if (column.headerRenderer && isComponentClass(column.headerRenderer)) {
1628
+ processed.headerRenderer = this.createComponentHeaderRenderer(column.headerRenderer);
1629
+ }
1630
+ // Convert headerLabelRenderer component class to function
1631
+ if (column.headerLabelRenderer && isComponentClass(column.headerLabelRenderer)) {
1632
+ processed.headerLabelRenderer = this.createComponentHeaderLabelRenderer(column.headerLabelRenderer);
1633
+ }
1624
1634
  return processed;
1625
1635
  }
1626
1636
  /**
@@ -2094,6 +2104,222 @@ class GridAdapter {
2094
2104
  return hostElement;
2095
2105
  };
2096
2106
  }
2107
+ /**
2108
+ * Creates a header renderer function from an Angular component class.
2109
+ * Mounts the component with full header context (column, value, sortState, etc.).
2110
+ * @internal
2111
+ */
2112
+ createComponentHeaderRenderer(componentClass) {
2113
+ return (ctx) => {
2114
+ const hostElement = document.createElement('span');
2115
+ hostElement.style.display = 'contents';
2116
+ const componentRef = createComponent(componentClass, {
2117
+ environmentInjector: this.injector,
2118
+ hostElement,
2119
+ });
2120
+ this.setComponentInputs(componentRef, {
2121
+ column: ctx.column,
2122
+ value: ctx.value,
2123
+ sortState: ctx.sortState,
2124
+ filterActive: ctx.filterActive,
2125
+ renderSortIcon: ctx.renderSortIcon,
2126
+ renderFilterButton: ctx.renderFilterButton,
2127
+ });
2128
+ this.appRef.attachView(componentRef.hostView);
2129
+ this.componentRefs.push(componentRef);
2130
+ componentRef.changeDetectorRef.detectChanges();
2131
+ return hostElement;
2132
+ };
2133
+ }
2134
+ /**
2135
+ * Creates a header label renderer function from an Angular component class.
2136
+ * Mounts the component with label context (column, value).
2137
+ * @internal
2138
+ */
2139
+ createComponentHeaderLabelRenderer(componentClass) {
2140
+ return (ctx) => {
2141
+ const hostElement = document.createElement('span');
2142
+ hostElement.style.display = 'contents';
2143
+ const componentRef = createComponent(componentClass, {
2144
+ environmentInjector: this.injector,
2145
+ hostElement,
2146
+ });
2147
+ this.setComponentInputs(componentRef, {
2148
+ column: ctx.column,
2149
+ value: ctx.value,
2150
+ });
2151
+ this.appRef.attachView(componentRef.hostView);
2152
+ this.componentRefs.push(componentRef);
2153
+ componentRef.changeDetectorRef.detectChanges();
2154
+ return hostElement;
2155
+ };
2156
+ }
2157
+ /**
2158
+ * Creates a group header renderer function from an Angular component class.
2159
+ *
2160
+ * The component should accept group header inputs (id, label, columns, firstIndex, isImplicit).
2161
+ * Returns the host element directly (groupHeaderRenderer returns an element, not void).
2162
+ * @internal
2163
+ */
2164
+ createComponentGroupHeaderRenderer(componentClass) {
2165
+ return (params) => {
2166
+ const hostElement = document.createElement('span');
2167
+ hostElement.style.display = 'contents';
2168
+ const componentRef = createComponent(componentClass, {
2169
+ environmentInjector: this.injector,
2170
+ hostElement,
2171
+ });
2172
+ this.setComponentInputs(componentRef, {
2173
+ id: params.id,
2174
+ label: params.label,
2175
+ columns: params.columns,
2176
+ firstIndex: params.firstIndex,
2177
+ isImplicit: params.isImplicit,
2178
+ });
2179
+ this.appRef.attachView(componentRef.hostView);
2180
+ this.componentRefs.push(componentRef);
2181
+ componentRef.changeDetectorRef.detectChanges();
2182
+ return hostElement;
2183
+ };
2184
+ }
2185
+ /**
2186
+ * Processes a GroupingColumnsConfig, converting component class references
2187
+ * to actual renderer functions.
2188
+ *
2189
+ * @param config - Angular grouping columns configuration with possible component class references
2190
+ * @returns Processed GroupingColumnsConfig with actual renderer functions
2191
+ */
2192
+ processGroupingColumnsConfig(config) {
2193
+ if (config.groupHeaderRenderer && isComponentClass(config.groupHeaderRenderer)) {
2194
+ return {
2195
+ ...config,
2196
+ groupHeaderRenderer: this.createComponentGroupHeaderRenderer(config.groupHeaderRenderer),
2197
+ };
2198
+ }
2199
+ return config;
2200
+ }
2201
+ /**
2202
+ * Processes a GroupingRowsConfig, converting component class references
2203
+ * to actual renderer functions.
2204
+ *
2205
+ * @param config - Angular grouping rows configuration with possible component class references
2206
+ * @returns Processed GroupingRowsConfig with actual renderer functions
2207
+ */
2208
+ processGroupingRowsConfig(config) {
2209
+ if (config.groupRowRenderer && isComponentClass(config.groupRowRenderer)) {
2210
+ return {
2211
+ ...config,
2212
+ groupRowRenderer: this.createComponentGroupRowRenderer(config.groupRowRenderer),
2213
+ };
2214
+ }
2215
+ return config;
2216
+ }
2217
+ /**
2218
+ * Processes a PinnedRowsConfig, converting component class references
2219
+ * in `customPanels[].render` to actual renderer functions.
2220
+ *
2221
+ * @param config - Angular pinned rows configuration with possible component class references
2222
+ * @returns Processed PinnedRowsConfig with actual renderer functions
2223
+ */
2224
+ processPinnedRowsConfig(config) {
2225
+ if (!Array.isArray(config.customPanels))
2226
+ return config;
2227
+ const hasComponentRender = config.customPanels.some((panel) => isComponentClass(panel.render));
2228
+ if (!hasComponentRender)
2229
+ return config;
2230
+ return {
2231
+ ...config,
2232
+ customPanels: config.customPanels.map((panel) => {
2233
+ if (!isComponentClass(panel.render))
2234
+ return panel;
2235
+ return {
2236
+ ...panel,
2237
+ render: this.createComponentPinnedRowsPanelRenderer(panel.render),
2238
+ };
2239
+ }),
2240
+ };
2241
+ }
2242
+ /**
2243
+ * Creates a pinned rows panel renderer function from an Angular component class.
2244
+ *
2245
+ * The component should accept inputs from PinnedRowsContext (totalRows, filteredRows,
2246
+ * selectedRows, columns, rows, grid).
2247
+ * @internal
2248
+ */
2249
+ createComponentPinnedRowsPanelRenderer(componentClass) {
2250
+ return (ctx) => {
2251
+ const hostElement = document.createElement('span');
2252
+ hostElement.style.display = 'contents';
2253
+ const componentRef = createComponent(componentClass, {
2254
+ environmentInjector: this.injector,
2255
+ hostElement,
2256
+ });
2257
+ this.setComponentInputs(componentRef, {
2258
+ totalRows: ctx.totalRows,
2259
+ filteredRows: ctx.filteredRows,
2260
+ selectedRows: ctx.selectedRows,
2261
+ columns: ctx.columns,
2262
+ rows: ctx.rows,
2263
+ grid: ctx.grid,
2264
+ });
2265
+ this.appRef.attachView(componentRef.hostView);
2266
+ this.componentRefs.push(componentRef);
2267
+ componentRef.changeDetectorRef.detectChanges();
2268
+ return hostElement;
2269
+ };
2270
+ }
2271
+ /**
2272
+ * Creates a loading renderer function from an Angular component class.
2273
+ *
2274
+ * The component should accept a `size` input ('large' | 'small').
2275
+ * @internal
2276
+ */
2277
+ createComponentLoadingRenderer(componentClass) {
2278
+ return (ctx) => {
2279
+ const hostElement = document.createElement('span');
2280
+ hostElement.style.display = 'contents';
2281
+ const componentRef = createComponent(componentClass, {
2282
+ environmentInjector: this.injector,
2283
+ hostElement,
2284
+ });
2285
+ this.setComponentInputs(componentRef, {
2286
+ size: ctx.size,
2287
+ });
2288
+ this.appRef.attachView(componentRef.hostView);
2289
+ this.componentRefs.push(componentRef);
2290
+ componentRef.changeDetectorRef.detectChanges();
2291
+ return hostElement;
2292
+ };
2293
+ }
2294
+ /**
2295
+ * Creates a group row renderer function from an Angular component class.
2296
+ *
2297
+ * The component should accept group row inputs (key, value, depth, rows, expanded, toggleExpand).
2298
+ * Returns the host element directly (groupRowRenderer returns an element, not void).
2299
+ * @internal
2300
+ */
2301
+ createComponentGroupRowRenderer(componentClass) {
2302
+ return (params) => {
2303
+ const hostElement = document.createElement('span');
2304
+ hostElement.style.display = 'contents';
2305
+ const componentRef = createComponent(componentClass, {
2306
+ environmentInjector: this.injector,
2307
+ hostElement,
2308
+ });
2309
+ this.setComponentInputs(componentRef, {
2310
+ key: params.key,
2311
+ value: params.value,
2312
+ depth: params.depth,
2313
+ rows: params.rows,
2314
+ expanded: params.expanded,
2315
+ toggleExpand: params.toggleExpand,
2316
+ });
2317
+ this.appRef.attachView(componentRef.hostView);
2318
+ this.componentRefs.push(componentRef);
2319
+ componentRef.changeDetectorRef.detectChanges();
2320
+ return hostElement;
2321
+ };
2322
+ }
2097
2323
  /**
2098
2324
  * Creates a filter panel renderer function from an Angular component class.
2099
2325
  *
@@ -2202,7 +2428,7 @@ class GridAdapter {
2202
2428
  }
2203
2429
  }
2204
2430
  /**
2205
- * @deprecated Use `GridAdapter` instead. This alias will be removed in a future version.
2431
+ * @deprecated Use `GridAdapter` instead. This alias will be removed in v2.
2206
2432
  * @see {@link GridAdapter}
2207
2433
  */
2208
2434
  const AngularGridAdapter = GridAdapter;
@@ -2580,7 +2806,7 @@ function injectGrid() {
2580
2806
  * ## Usage
2581
2807
  *
2582
2808
  * ```typescript
2583
- * import { Component } from '@angular/core';
2809
+ * import { Component, viewChild, ElementRef } from '@angular/core';
2584
2810
  * import { BaseFilterPanel } from '@toolbox-web/grid-angular';
2585
2811
  *
2586
2812
  * @Component({
@@ -2592,10 +2818,10 @@ function injectGrid() {
2592
2818
  * `
2593
2819
  * })
2594
2820
  * export class TextFilterComponent extends BaseFilterPanel {
2595
- * @ViewChild('input') input!: ElementRef<HTMLInputElement>;
2821
+ * input = viewChild.required<ElementRef<HTMLInputElement>>('input');
2596
2822
  *
2597
2823
  * applyFilter(): void {
2598
- * this.params().applyTextFilter('contains', this.input.nativeElement.value);
2824
+ * this.params().applyTextFilter('contains', this.input().nativeElement.value);
2599
2825
  * }
2600
2826
  * }
2601
2827
  * ```
@@ -2832,13 +3058,21 @@ class BaseGridEditor {
2832
3058
  const grid = this.elementRef.nativeElement.closest('tbw-grid');
2833
3059
  if (!grid)
2834
3060
  return;
2835
- const beforeHandler = () => this.onBeforeEditClose();
2836
- grid.addEventListener('before-edit-close', beforeHandler, { once: true });
2837
- const handler = () => this.onEditClose();
2838
- grid.addEventListener('edit-close', handler, { once: true });
3061
+ let unsubBefore;
3062
+ let unsubClose;
3063
+ unsubBefore = grid.on('before-edit-close', () => {
3064
+ this.onBeforeEditClose();
3065
+ unsubBefore?.();
3066
+ unsubBefore = undefined;
3067
+ });
3068
+ unsubClose = grid.on('edit-close', () => {
3069
+ this.onEditClose();
3070
+ unsubClose?.();
3071
+ unsubClose = undefined;
3072
+ });
2839
3073
  this._editCloseCleanup = () => {
2840
- grid.removeEventListener('before-edit-close', beforeHandler);
2841
- grid.removeEventListener('edit-close', handler);
3074
+ unsubBefore?.();
3075
+ unsubClose?.();
2842
3076
  };
2843
3077
  }
2844
3078
  // ============================================================================
@@ -3188,7 +3422,7 @@ let anchorCounter = 0;
3188
3422
  * ## Usage
3189
3423
  *
3190
3424
  * ```typescript
3191
- * import { Component, ViewChild, ElementRef } from '@angular/core';
3425
+ * import { Component, viewChild, ElementRef, effect } from '@angular/core';
3192
3426
  * import { BaseOverlayEditor } from '@toolbox-web/grid-angular';
3193
3427
  *
3194
3428
  * @Component({
@@ -3211,18 +3445,22 @@ let anchorCounter = 0;
3211
3445
  * `
3212
3446
  * })
3213
3447
  * export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> {
3214
- * @ViewChild('panel') panelRef!: ElementRef<HTMLElement>;
3215
- * @ViewChild('inlineInput') inputRef!: ElementRef<HTMLInputElement>;
3448
+ * panelRef = viewChild.required<ElementRef<HTMLElement>>('panel');
3449
+ * inputRef = viewChild.required<ElementRef<HTMLInputElement>>('inlineInput');
3216
3450
  *
3217
3451
  * protected override overlayPosition = 'below' as const;
3218
3452
  *
3219
- * ngAfterViewInit(): void {
3220
- * this.initOverlay(this.panelRef.nativeElement);
3221
- * if (this.isCellFocused()) this.showOverlay();
3453
+ * constructor() {
3454
+ * super();
3455
+ * effect(() => {
3456
+ * const panel = this.panelRef().nativeElement;
3457
+ * this.initOverlay(panel);
3458
+ * if (this.isCellFocused()) this.showOverlay();
3459
+ * });
3222
3460
  * }
3223
3461
  *
3224
3462
  * protected getInlineInput(): HTMLInputElement | null {
3225
- * return this.inputRef?.nativeElement ?? null;
3463
+ * return this.inputRef()?.nativeElement ?? null;
3226
3464
  * }
3227
3465
  *
3228
3466
  * protected onOverlayOutsideClick(): void {
@@ -3283,7 +3521,7 @@ class BaseOverlayEditor extends BaseGridEditor {
3283
3521
  /**
3284
3522
  * Initialise the overlay with the panel element.
3285
3523
  *
3286
- * Call this in `ngAfterViewInit` with your `@ViewChild` panel reference.
3524
+ * Call this in an `effect()` or `afterNextRender()` with your `viewChild` panel reference.
3287
3525
  * The panel is moved to `<body>` and hidden until {@link showOverlay} is called.
3288
3526
  *
3289
3527
  * @param panel - The overlay panel DOM element
@@ -3801,9 +4039,9 @@ class GridLazyForm {
3801
4039
  rowIndexMap = new Map();
3802
4040
  // Track which row is currently being edited
3803
4041
  editingRowIndex = null;
3804
- cellCommitListener = null;
3805
- rowCommitListener = null;
3806
- rowsChangeListener = null;
4042
+ cellCommitUnsub = null;
4043
+ rowCommitUnsub = null;
4044
+ rowsChangeUnsub = null;
3807
4045
  /**
3808
4046
  * Factory function to create a FormGroup for a row.
3809
4047
  * Called lazily when the row first enters edit mode.
@@ -3850,22 +4088,17 @@ class GridLazyForm {
3850
4088
  // Store the form context for AngularGridAdapter to access
3851
4089
  this.#storeFormContext(grid);
3852
4090
  // Listen for cell-commit to update FormControl and sync validation
3853
- this.cellCommitListener = (e) => {
3854
- const detail = e.detail;
4091
+ this.cellCommitUnsub = grid.on('cell-commit', (detail) => {
3855
4092
  this.#handleCellCommit(detail);
3856
- };
3857
- grid.addEventListener('cell-commit', this.cellCommitListener);
4093
+ });
3858
4094
  // Listen for row-commit to sync FormGroup values back to row and cleanup
3859
- this.rowCommitListener = (e) => {
3860
- const detail = e.detail;
3861
- this.#handleRowCommit(e, detail);
3862
- };
3863
- grid.addEventListener('row-commit', this.rowCommitListener);
4095
+ this.rowCommitUnsub = grid.on('row-commit', (detail, event) => {
4096
+ this.#handleRowCommit(event, detail);
4097
+ });
3864
4098
  // Listen for rows-change to update row index mappings
3865
- this.rowsChangeListener = () => {
4099
+ this.rowsChangeUnsub = grid.on('rows-change', () => {
3866
4100
  this.#updateRowIndexMap();
3867
- };
3868
- grid.addEventListener('rows-change', this.rowsChangeListener);
4101
+ });
3869
4102
  // Initial row index mapping
3870
4103
  this.#updateRowIndexMap();
3871
4104
  }
@@ -3873,15 +4106,9 @@ class GridLazyForm {
3873
4106
  const grid = this.elementRef.nativeElement;
3874
4107
  if (!grid)
3875
4108
  return;
3876
- if (this.cellCommitListener) {
3877
- grid.removeEventListener('cell-commit', this.cellCommitListener);
3878
- }
3879
- if (this.rowCommitListener) {
3880
- grid.removeEventListener('row-commit', this.rowCommitListener);
3881
- }
3882
- if (this.rowsChangeListener) {
3883
- grid.removeEventListener('rows-change', this.rowsChangeListener);
3884
- }
4109
+ this.cellCommitUnsub?.();
4110
+ this.rowCommitUnsub?.();
4111
+ this.rowsChangeUnsub?.();
3885
4112
  this.#clearFormContext(grid);
3886
4113
  this.formGroupCache.clear();
3887
4114
  this.rowIndexMap.clear();
@@ -4565,7 +4792,7 @@ class Grid {
4565
4792
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4566
4793
  gridConfig = input(...(ngDevMode ? [undefined, { debugName: "gridConfig" }] : []));
4567
4794
  /**
4568
- * @deprecated Use `gridConfig` instead. This input will be removed in a future version.
4795
+ * @deprecated Use `gridConfig` instead. This input will be removed in v2.
4569
4796
  *
4570
4797
  * The `angularConfig` name was inconsistent with React and Vue adapters, which both use `gridConfig`.
4571
4798
  * The `gridConfig` input now accepts `GridConfig` directly.
@@ -4678,7 +4905,7 @@ class Grid {
4678
4905
  */
4679
4906
  multiSort = input(...(ngDevMode ? [undefined, { debugName: "multiSort" }] : []));
4680
4907
  /**
4681
- * @deprecated Use `[multiSort]` instead. Will be removed in a future version.
4908
+ * @deprecated Use `[multiSort]` instead. Will be removed in v2.
4682
4909
  *
4683
4910
  * Enable column sorting. This is an alias for `[multiSort]`.
4684
4911
  *
@@ -4719,7 +4946,7 @@ class Grid {
4719
4946
  */
4720
4947
  reorderColumns = input(...(ngDevMode ? [undefined, { debugName: "reorderColumns" }] : []));
4721
4948
  /**
4722
- * @deprecated Use `reorderColumns` instead. Will be removed in v2.0.
4949
+ * @deprecated Use `reorderColumns` instead. Will be removed in v2.
4723
4950
  */
4724
4951
  reorder = input(...(ngDevMode ? [undefined, { debugName: "reorder" }] : []));
4725
4952
  /**
@@ -4765,7 +4992,7 @@ class Grid {
4765
4992
  *
4766
4993
  * @example
4767
4994
  * ```html
4768
- * <tbw-grid [groupingColumns]="{ columnGroups: [...] }" />
4995
+ * <tbw-grid [groupingColumns]="true" />
4769
4996
  * ```
4770
4997
  */
4771
4998
  groupingColumns = input(...(ngDevMode ? [undefined, { debugName: "groupingColumns" }] : []));
@@ -5220,54 +5447,13 @@ class Grid {
5220
5447
  this.adapter = new GridAdapter(this.injector, this.appRef, this.viewContainerRef);
5221
5448
  DataGridElement.registerAdapter(this.adapter);
5222
5449
  const grid = this.elementRef.nativeElement;
5223
- // Intercept the element's gridConfig setter so that ALL writes
5224
- // (including Angular's own template property binding when CUSTOM_ELEMENTS_SCHEMA
5225
- // is used) go through the adapter's processGridConfig first.
5226
- // This converts Angular component classes to vanilla renderer/editor functions
5227
- // before the grid's internal ConfigManager ever sees them.
5228
- this.interceptElementGridConfig(grid);
5229
- // Wire up all event listeners based on eventOutputMap
5230
- this.setupEventListeners(grid);
5231
- // Register adapter on the grid element so MasterDetailPlugin can use it
5232
- // via the __frameworkAdapter hook during attach()
5450
+ // Register adapter on the grid element so processConfig is called
5451
+ // automatically by the grid's set gridConfig setter, and so
5452
+ // MasterDetailPlugin can use it via the __frameworkAdapter hook during attach()
5233
5453
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5234
5454
  grid.__frameworkAdapter = this.adapter;
5235
- }
5236
- /**
5237
- * Overrides the element's `gridConfig` property so every write is processed
5238
- * through the adapter before reaching the grid core.
5239
- *
5240
- * Why: Angular with `CUSTOM_ELEMENTS_SCHEMA` may bind `[gridConfig]` to both
5241
- * the directive input AND the native custom-element property. The directive
5242
- * input feeds an effect that merges feature plugins, but the native property
5243
- * receives the raw config (with component classes as editors/renderers).
5244
- * Intercepting the setter guarantees only processed configs reach the grid.
5245
- */
5246
- interceptElementGridConfig(grid) {
5247
- const proto = Object.getPrototypeOf(grid);
5248
- const desc = Object.getOwnPropertyDescriptor(proto, 'gridConfig');
5249
- if (!desc?.set || !desc?.get)
5250
- return;
5251
- const originalSet = desc.set;
5252
- const originalGet = desc.get;
5253
- const adapter = this.adapter;
5254
- // Instance-level override (does not affect the prototype or other grid elements)
5255
- Object.defineProperty(grid, 'gridConfig', {
5256
- get() {
5257
- return originalGet.call(this);
5258
- },
5259
- set(value) {
5260
- if (value && adapter) {
5261
- // processGridConfig is idempotent: already-processed functions pass
5262
- // through isComponentClass unchanged, so double-processing is safe.
5263
- originalSet.call(this, adapter.processGridConfig(value));
5264
- }
5265
- else {
5266
- originalSet.call(this, value);
5267
- }
5268
- },
5269
- configurable: true,
5270
- });
5455
+ // Wire up all event listeners based on eventOutputMap
5456
+ this.setupEventListeners(grid);
5271
5457
  }
5272
5458
  /**
5273
5459
  * Sets up event listeners for all outputs using the eventOutputMap.
@@ -5311,11 +5497,32 @@ class Grid {
5311
5497
  addPlugin('reorderColumns', this.reorderColumns() ?? this.reorder());
5312
5498
  addPlugin('visibility', this.visibility());
5313
5499
  addPlugin('pinnedColumns', this.pinnedColumns());
5314
- addPlugin('groupingColumns', this.groupingColumns());
5500
+ // Pre-process groupingColumns config to bridge Angular component classes
5501
+ const gcConfig = this.groupingColumns();
5502
+ if (gcConfig && typeof gcConfig === 'object' && this.adapter) {
5503
+ addPlugin('groupingColumns', this.adapter.processGroupingColumnsConfig(gcConfig));
5504
+ }
5505
+ else {
5506
+ addPlugin('groupingColumns', gcConfig);
5507
+ }
5315
5508
  addPlugin('columnVirtualization', this.columnVirtualization());
5316
5509
  addPlugin('reorderRows', this.reorderRows() ?? this.rowReorder());
5317
- addPlugin('groupingRows', this.groupingRows());
5318
- addPlugin('pinnedRows', this.pinnedRows());
5510
+ // Pre-process groupingRows config to bridge Angular component classes
5511
+ const grConfig = this.groupingRows();
5512
+ if (grConfig && typeof grConfig === 'object' && this.adapter) {
5513
+ addPlugin('groupingRows', this.adapter.processGroupingRowsConfig(grConfig));
5514
+ }
5515
+ else {
5516
+ addPlugin('groupingRows', grConfig);
5517
+ }
5518
+ // Pre-process pinnedRows config to bridge Angular component classes in customPanels
5519
+ const prConfig = this.pinnedRows();
5520
+ if (prConfig && typeof prConfig === 'object' && this.adapter) {
5521
+ addPlugin('pinnedRows', this.adapter.processPinnedRowsConfig(prConfig));
5522
+ }
5523
+ else {
5524
+ addPlugin('pinnedRows', prConfig);
5525
+ }
5319
5526
  addPlugin('tree', this.tree());
5320
5527
  addPlugin('masterDetail', this.masterDetail());
5321
5528
  addPlugin('responsive', this.responsive());
@@ -5442,10 +5649,6 @@ class Grid {
5442
5649
  }
5443
5650
  ngOnDestroy() {
5444
5651
  const grid = this.elementRef.nativeElement;
5445
- // Remove the gridConfig interceptor (restores prototype behavior)
5446
- if (grid) {
5447
- delete grid['gridConfig'];
5448
- }
5449
5652
  // Cleanup all event listeners
5450
5653
  if (grid) {
5451
5654
  for (const [eventName, listener] of this.eventListeners) {