@toolbox-web/grid-angular 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, contentChild, TemplateRef, effect, Directive, input, InjectionToken, Injectable, makeEnvironmentProviders, EventEmitter, createComponent, signal, afterNextRender, computed, output, EnvironmentInjector, ApplicationRef, ViewContainerRef } from '@angular/core';
2
+ import { inject, ElementRef, contentChild, TemplateRef, effect, Directive, input, DestroyRef, InjectionToken, Injectable, makeEnvironmentProviders, EventEmitter, createComponent, signal, afterNextRender, computed, output, EnvironmentInjector, ApplicationRef, ViewContainerRef } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3
4
  import { FormGroup } from '@angular/forms';
5
+ import { startWith } from 'rxjs/operators';
4
6
  import { DataGridElement } from '@toolbox-web/grid';
5
7
 
6
8
  /**
@@ -339,10 +341,12 @@ function getFormArrayContext(gridElement) {
339
341
  * - Automatically syncs FormArray changes to the grid
340
342
  */
341
343
  class GridFormArray {
344
+ destroyRef = inject(DestroyRef);
342
345
  elementRef = inject((ElementRef));
343
346
  cellCommitListener = null;
344
347
  rowCommitListener = null;
345
348
  touchListener = null;
349
+ valueChangesSubscription = null;
346
350
  /**
347
351
  * The FormArray to bind to the grid.
348
352
  */
@@ -359,15 +363,24 @@ class GridFormArray {
359
363
  */
360
364
  syncValidation = input(true, ...(ngDevMode ? [{ debugName: "syncValidation" }] : []));
361
365
  /**
362
- * Effect that syncs the FormArray value to the grid rows.
366
+ * Effect that sets up valueChanges subscription when FormArray changes.
367
+ * This handles both initial binding and when the FormArray reference changes.
363
368
  */
364
369
  syncFormArrayToGrid = effect(() => {
365
370
  const formArray = this.formArray();
366
371
  const grid = this.elementRef.nativeElement;
367
- if (grid && formArray) {
368
- // Get the raw value (including disabled controls)
372
+ if (!grid || !formArray)
373
+ return;
374
+ // Unsubscribe from previous FormArray if any
375
+ this.valueChangesSubscription?.unsubscribe();
376
+ // Subscribe to valueChanges to sync grid rows when FormArray content changes.
377
+ // Use startWith to immediately sync the current value.
378
+ // Note: We use getRawValue() to include disabled controls.
379
+ this.valueChangesSubscription = formArray.valueChanges
380
+ .pipe(startWith(formArray.getRawValue()), takeUntilDestroyed(this.destroyRef))
381
+ .subscribe(() => {
369
382
  grid.rows = formArray.getRawValue();
370
- }
383
+ });
371
384
  }, ...(ngDevMode ? [{ debugName: "syncFormArrayToGrid" }] : []));
372
385
  ngOnInit() {
373
386
  const grid = this.elementRef.nativeElement;
@@ -413,6 +426,9 @@ class GridFormArray {
413
426
  if (this.touchListener) {
414
427
  grid.removeEventListener('click', this.touchListener);
415
428
  }
429
+ if (this.valueChangesSubscription) {
430
+ this.valueChangesSubscription.unsubscribe();
431
+ }
416
432
  this.#clearFormContext(grid);
417
433
  }
418
434
  /**
@@ -1268,14 +1284,39 @@ class AngularGridAdapter {
1268
1284
  * @returns Processed GridConfig with actual renderer/editor functions
1269
1285
  */
1270
1286
  processGridConfig(config) {
1271
- if (!config.columns) {
1272
- return config;
1287
+ const result = { ...config };
1288
+ // Process columns
1289
+ if (config.columns) {
1290
+ result.columns = config.columns.map((col) => this.processColumn(col));
1273
1291
  }
1274
- const processedColumns = config.columns.map((col) => this.processColumn(col));
1275
- return {
1276
- ...config,
1277
- columns: processedColumns,
1278
- };
1292
+ // Process typeDefaults - convert Angular component classes to renderer/editor functions
1293
+ if (config.typeDefaults) {
1294
+ result.typeDefaults = this.processTypeDefaults(config.typeDefaults);
1295
+ }
1296
+ return result;
1297
+ }
1298
+ /**
1299
+ * Processes typeDefaults configuration, converting component class references
1300
+ * to actual renderer/editor functions.
1301
+ *
1302
+ * @param typeDefaults - Angular type defaults with possible component class references
1303
+ * @returns Processed TypeDefault record
1304
+ */
1305
+ processTypeDefaults(typeDefaults) {
1306
+ const processed = {};
1307
+ for (const [type, config] of Object.entries(typeDefaults)) {
1308
+ const processedConfig = { ...config };
1309
+ // Convert renderer component class to function
1310
+ if (config.renderer && isComponentClass(config.renderer)) {
1311
+ processedConfig.renderer = this.createComponentRenderer(config.renderer);
1312
+ }
1313
+ // Convert editor component class to function
1314
+ if (config.editor && isComponentClass(config.editor)) {
1315
+ processedConfig.editor = this.createComponentEditor(config.editor);
1316
+ }
1317
+ processed[type] = processedConfig;
1318
+ }
1319
+ return processed;
1279
1320
  }
1280
1321
  /**
1281
1322
  * Processes a single column configuration, converting component class references
@@ -1318,6 +1359,11 @@ class AngularGridAdapter {
1318
1359
  return undefined;
1319
1360
  }
1320
1361
  return (ctx) => {
1362
+ // Skip rendering if the cell is in editing mode
1363
+ // This prevents the renderer from overwriting the editor when the grid re-renders
1364
+ if (ctx.cellEl?.classList.contains('editing')) {
1365
+ return null;
1366
+ }
1321
1367
  // Create the context for the template
1322
1368
  const context = {
1323
1369
  $implicit: ctx.value,
@@ -1358,10 +1404,10 @@ class AngularGridAdapter {
1358
1404
  // Find the parent grid element for FormArray context access
1359
1405
  const gridElement = element.closest('tbw-grid');
1360
1406
  if (!template) {
1361
- // No warning - this can happen during early initialization before Angular
1362
- // registers structural directive templates. The grid will re-parse columns
1363
- // after templates are registered.
1364
- return () => document.createElement('div');
1407
+ // No template registered - return undefined to let the grid use its default editor.
1408
+ // This allows columns with only *tbwRenderer (no *tbwEditor) to still be editable
1409
+ // using the built-in text/number/boolean editors.
1410
+ return undefined;
1365
1411
  }
1366
1412
  return (ctx) => {
1367
1413
  // Create simple callback functions (preferred)
@@ -1591,31 +1637,58 @@ class AngularGridAdapter {
1591
1637
  }
1592
1638
  return typeDefault;
1593
1639
  }
1640
+ /**
1641
+ * Creates and mounts an Angular component dynamically.
1642
+ * Shared logic between renderer and editor component creation.
1643
+ * @internal
1644
+ */
1645
+ mountComponent(componentClass, inputs) {
1646
+ // Create a host element for the component
1647
+ const hostElement = document.createElement('span');
1648
+ hostElement.style.display = 'contents';
1649
+ // Create the component dynamically
1650
+ const componentRef = createComponent(componentClass, {
1651
+ environmentInjector: this.injector,
1652
+ hostElement,
1653
+ });
1654
+ // Set inputs - components should have value, row, column inputs
1655
+ this.setComponentInputs(componentRef, inputs);
1656
+ // Attach to app for change detection
1657
+ this.appRef.attachView(componentRef.hostView);
1658
+ this.componentRefs.push(componentRef);
1659
+ // Trigger change detection
1660
+ componentRef.changeDetectorRef.detectChanges();
1661
+ return { hostElement, componentRef };
1662
+ }
1663
+ /**
1664
+ * Wires up commit/cancel handlers for an editor component.
1665
+ * Supports both Angular outputs and DOM CustomEvents.
1666
+ * @internal
1667
+ */
1668
+ wireEditorCallbacks(hostElement, componentRef, commit, cancel) {
1669
+ // Subscribe to Angular outputs (commit/cancel) on the component instance.
1670
+ // This works with Angular's output() signal API.
1671
+ const instance = componentRef.instance;
1672
+ this.subscribeToOutput(instance, 'commit', commit);
1673
+ this.subscribeToOutput(instance, 'cancel', cancel);
1674
+ // Also listen for DOM events as fallback (for components that dispatch CustomEvents)
1675
+ hostElement.addEventListener('commit', (e) => {
1676
+ const customEvent = e;
1677
+ commit(customEvent.detail);
1678
+ });
1679
+ hostElement.addEventListener('cancel', () => cancel());
1680
+ }
1594
1681
  /**
1595
1682
  * Creates a renderer function from an Angular component class.
1596
1683
  * @internal
1597
1684
  */
1598
1685
  createComponentRenderer(componentClass) {
1599
1686
  return (ctx) => {
1600
- // Create a host element for the component
1601
- const hostElement = document.createElement('span');
1602
- hostElement.style.display = 'contents';
1603
- // Create the component dynamically
1604
- const componentRef = createComponent(componentClass, {
1605
- environmentInjector: this.injector,
1606
- hostElement,
1607
- });
1608
- // Set inputs - components should have value, row, column inputs
1609
- this.setComponentInputs(componentRef, {
1687
+ const { hostElement } = this.mountComponent(componentClass, {
1610
1688
  value: ctx.value,
1611
1689
  row: ctx.row,
1612
1690
  column: ctx.column,
1613
1691
  });
1614
- // Attach to app for change detection
1615
- this.appRef.attachView(componentRef.hostView);
1616
- this.componentRefs.push(componentRef);
1617
- // Trigger change detection
1618
- componentRef.changeDetectorRef.detectChanges();
1619
1692
  return hostElement;
1620
1693
  };
1621
1694
  }
@@ -1625,38 +1698,12 @@ class AngularGridAdapter {
1625
1698
  */
1626
1699
  createComponentEditor(componentClass) {
1627
1700
  return (ctx) => {
1628
- // Create a host element for the component
1629
- const hostElement = document.createElement('span');
1630
- hostElement.style.display = 'contents';
1631
- // Create the component dynamically
1632
- const componentRef = createComponent(componentClass, {
1633
- environmentInjector: this.injector,
1634
- hostElement,
1635
- });
1636
- // Set inputs - components should have value, row, column inputs
1637
- this.setComponentInputs(componentRef, {
1701
+ const { hostElement, componentRef } = this.mountComponent(componentClass, {
1638
1702
  value: ctx.value,
1639
1703
  row: ctx.row,
1640
1704
  column: ctx.column,
1641
1705
  });
1642
- // Attach to app for change detection
1643
- this.appRef.attachView(componentRef.hostView);
1644
- this.componentRefs.push(componentRef);
1645
- // Trigger change detection
1646
- componentRef.changeDetectorRef.detectChanges();
1647
- // Subscribe to Angular outputs (commit/cancel) on the component instance.
1648
- // This works with Angular's output() signal API.
1649
- const instance = componentRef.instance;
1650
- this.subscribeToOutput(instance, 'commit', (value) => ctx.commit(value));
1651
- this.subscribeToOutput(instance, 'cancel', () => ctx.cancel());
1652
- // Also listen for DOM events as fallback (for components that dispatch CustomEvents)
1653
- hostElement.addEventListener('commit', (e) => {
1654
- const customEvent = e;
1655
- ctx.commit(customEvent.detail);
1656
- });
1657
- hostElement.addEventListener('cancel', () => {
1658
- ctx.cancel();
1659
- });
1706
+ this.wireEditorCallbacks(hostElement, componentRef, (value) => ctx.commit(value), () => ctx.cancel());
1660
1707
  return hostElement;
1661
1708
  };
1662
1709
  }
@@ -2697,6 +2744,9 @@ class Grid {
2697
2744
  * <tbw-grid [editing]="'click'" />
2698
2745
  * <tbw-grid [editing]="'dblclick'" />
2699
2746
  * <tbw-grid [editing]="'manual'" />
2747
+ *
2748
+ * <!-- Full config with callbacks -->
2749
+ * <tbw-grid [editing]="{ editOn: 'dblclick', onBeforeEditClose: myCallback }" />
2700
2750
  * ```
2701
2751
  */
2702
2752
  editing = input(...(ngDevMode ? [undefined, { debugName: "editing" }] : []));