@toolbox-web/grid-angular 0.11.0 → 0.11.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.
@@ -1457,12 +1457,31 @@ class GridAdapter {
1457
1457
  // This is important when only an editor template is provided (no view template)
1458
1458
  return undefined;
1459
1459
  }
1460
+ // Cell cache for this column - maps cell element to its view ref and root node.
1461
+ // When the grid recycles pool elements during scroll, the same cellEl is reused
1462
+ // for different row data. By caching per cellEl, we reuse the Angular view and
1463
+ // just update its context instead of creating a new embedded view every time.
1464
+ // This matches what React and Vue adapters do with their cell caches.
1465
+ const cellCache = new WeakMap();
1460
1466
  return (ctx) => {
1461
1467
  // Skip rendering if the cell is in editing mode
1462
1468
  // This prevents the renderer from overwriting the editor when the grid re-renders
1463
1469
  if (ctx.cellEl?.classList.contains('editing')) {
1464
1470
  return null;
1465
1471
  }
1472
+ const cellEl = ctx.cellEl;
1473
+ if (cellEl) {
1474
+ const cached = cellCache.get(cellEl);
1475
+ if (cached) {
1476
+ // Reuse existing view - just update context and re-run change detection
1477
+ cached.viewRef.context.$implicit = ctx.value;
1478
+ cached.viewRef.context.value = ctx.value;
1479
+ cached.viewRef.context.row = ctx.row;
1480
+ cached.viewRef.context.column = ctx.column;
1481
+ cached.viewRef.detectChanges();
1482
+ return cached.rootNode;
1483
+ }
1484
+ }
1466
1485
  // Create the context for the template
1467
1486
  const context = {
1468
1487
  $implicit: ctx.value,
@@ -1477,6 +1496,10 @@ class GridAdapter {
1477
1496
  viewRef.detectChanges();
1478
1497
  // Get the first root node (the component's host element)
1479
1498
  const rootNode = viewRef.rootNodes[0];
1499
+ // Cache for reuse on scroll recycles
1500
+ if (cellEl) {
1501
+ cellCache.set(cellEl, { viewRef, rootNode });
1502
+ }
1480
1503
  return rootNode;
1481
1504
  };
1482
1505
  }
@@ -1782,12 +1805,32 @@ class GridAdapter {
1782
1805
  * @internal
1783
1806
  */
1784
1807
  createComponentRenderer(componentClass) {
1808
+ // Cell cache for component-based renderers - maps cell element to its component ref
1809
+ const cellCache = new WeakMap();
1785
1810
  return (ctx) => {
1786
- const { hostElement } = this.mountComponent(componentClass, {
1811
+ const cellEl = ctx.cellEl;
1812
+ if (cellEl) {
1813
+ const cached = cellCache.get(cellEl);
1814
+ if (cached) {
1815
+ // Reuse existing component - just update inputs
1816
+ this.setComponentInputs(cached.componentRef, {
1817
+ value: ctx.value,
1818
+ row: ctx.row,
1819
+ column: ctx.column,
1820
+ });
1821
+ cached.componentRef.changeDetectorRef.detectChanges();
1822
+ return cached.hostElement;
1823
+ }
1824
+ }
1825
+ const { hostElement, componentRef } = this.mountComponent(componentClass, {
1787
1826
  value: ctx.value,
1788
1827
  row: ctx.row,
1789
1828
  column: ctx.column,
1790
1829
  });
1830
+ // Cache for reuse on scroll recycles
1831
+ if (cellEl) {
1832
+ cellCache.set(cellEl, { componentRef, hostElement });
1833
+ }
1791
1834
  return hostElement;
1792
1835
  };
1793
1836
  }
@@ -3093,30 +3136,24 @@ class Grid {
3093
3136
  const existingIcons = angularCfg?.icons || {};
3094
3137
  coreConfigOverrides['icons'] = { ...registryIcons, ...existingIcons };
3095
3138
  }
3096
- // If gridConfig is provided, process it (converts component classes to renderer functions)
3097
- const processedConfig = angularCfg ? this.adapter.processGridConfig(angularCfg) : null;
3098
- // IMPORTANT: If user is NOT using gridConfig input, and there are no feature plugins
3099
- // or config overrides to merge, do NOT overwrite grid.gridConfig.
3100
- // This allows [gridConfig]="myConfig" binding to work correctly without the directive
3101
- // creating a new object that strips properties like typeDefaults.
3139
+ // Nothing to do if there's no config input and no feature inputs
3102
3140
  const hasFeaturePlugins = featurePlugins.length > 0;
3103
3141
  const hasConfigOverrides = Object.keys(coreConfigOverrides).length > 0;
3104
- // The input signal gives us reactive tracking of the user's config
3105
- const existingConfig = angularCfg || {};
3106
- if (!processedConfig && !hasFeaturePlugins && !hasConfigOverrides && !angularCfg) {
3107
- // Nothing to merge and no config input - let the user's DOM binding work directly
3142
+ if (!angularCfg && !hasFeaturePlugins && !hasConfigOverrides) {
3108
3143
  return;
3109
3144
  }
3110
- // Merge: processed config < feature plugins
3111
- const configPlugins = processedConfig?.plugins || existingConfig.plugins || [];
3145
+ const userConfig = angularCfg || {};
3146
+ // Merge feature-input plugins with the user's own plugins
3147
+ const configPlugins = userConfig.plugins || [];
3112
3148
  const mergedPlugins = [...featurePlugins, ...configPlugins];
3113
- // Build the final config, preserving ALL existing properties (including typeDefaults)
3114
- const baseConfig = processedConfig || existingConfig;
3149
+ // The interceptor on element.gridConfig (installed in ngOnInit)
3150
+ // handles converting component classes → functions via processGridConfig,
3151
+ // so we can pass the raw Angular config through. The interceptor is
3152
+ // idempotent, making this safe even if the config is already processed.
3115
3153
  grid.gridConfig = {
3116
- ...existingConfig, // Start with existing config to preserve all properties (including typeDefaults)
3117
- ...baseConfig, // Then apply processed/angular config
3154
+ ...userConfig,
3118
3155
  ...coreConfigOverrides,
3119
- plugins: mergedPlugins.length > 0 ? mergedPlugins : baseConfig.plugins,
3156
+ plugins: mergedPlugins.length > 0 ? mergedPlugins : userConfig.plugins,
3120
3157
  };
3121
3158
  });
3122
3159
  // Effect to sync loading state to the grid element
@@ -3921,6 +3958,12 @@ class Grid {
3921
3958
  this.adapter = new GridAdapter(this.injector, this.appRef, this.viewContainerRef);
3922
3959
  DataGridElement.registerAdapter(this.adapter);
3923
3960
  const grid = this.elementRef.nativeElement;
3961
+ // Intercept the element's gridConfig setter so that ALL writes
3962
+ // (including Angular's own template property binding when CUSTOM_ELEMENTS_SCHEMA
3963
+ // is used) go through the adapter's processGridConfig first.
3964
+ // This converts Angular component classes to vanilla renderer/editor functions
3965
+ // before the grid's internal ConfigManager ever sees them.
3966
+ this.interceptElementGridConfig(grid);
3924
3967
  // Wire up all event listeners based on eventOutputMap
3925
3968
  this.setupEventListeners(grid);
3926
3969
  // Register adapter on the grid element so MasterDetailPlugin can use it
@@ -3928,6 +3971,42 @@ class Grid {
3928
3971
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3929
3972
  grid.__frameworkAdapter = this.adapter;
3930
3973
  }
3974
+ /**
3975
+ * Overrides the element's `gridConfig` property so every write is processed
3976
+ * through the adapter before reaching the grid core.
3977
+ *
3978
+ * Why: Angular with `CUSTOM_ELEMENTS_SCHEMA` may bind `[gridConfig]` to both
3979
+ * the directive input AND the native custom-element property. The directive
3980
+ * input feeds an effect that merges feature plugins, but the native property
3981
+ * receives the raw config (with component classes as editors/renderers).
3982
+ * Intercepting the setter guarantees only processed configs reach the grid.
3983
+ */
3984
+ interceptElementGridConfig(grid) {
3985
+ const proto = Object.getPrototypeOf(grid);
3986
+ const desc = Object.getOwnPropertyDescriptor(proto, 'gridConfig');
3987
+ if (!desc?.set || !desc?.get)
3988
+ return;
3989
+ const originalSet = desc.set;
3990
+ const originalGet = desc.get;
3991
+ const adapter = this.adapter;
3992
+ // Instance-level override (does not affect the prototype or other grid elements)
3993
+ Object.defineProperty(grid, 'gridConfig', {
3994
+ get() {
3995
+ return originalGet.call(this);
3996
+ },
3997
+ set(value) {
3998
+ if (value && adapter) {
3999
+ // processGridConfig is idempotent: already-processed functions pass
4000
+ // through isComponentClass unchanged, so double-processing is safe.
4001
+ originalSet.call(this, adapter.processGridConfig(value));
4002
+ }
4003
+ else {
4004
+ originalSet.call(this, value);
4005
+ }
4006
+ },
4007
+ configurable: true,
4008
+ });
4009
+ }
3931
4010
  /**
3932
4011
  * Sets up event listeners for all outputs using the eventOutputMap.
3933
4012
  */
@@ -4101,6 +4180,10 @@ class Grid {
4101
4180
  }
4102
4181
  ngOnDestroy() {
4103
4182
  const grid = this.elementRef.nativeElement;
4183
+ // Remove the gridConfig interceptor (restores prototype behavior)
4184
+ if (grid) {
4185
+ delete grid['gridConfig'];
4186
+ }
4104
4187
  // Cleanup all event listeners
4105
4188
  if (grid) {
4106
4189
  for (const [eventName, listener] of this.eventListeners) {