@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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
3111
|
-
|
|
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
|
-
//
|
|
3114
|
-
|
|
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
|
-
...
|
|
3117
|
-
...baseConfig, // Then apply processed/angular config
|
|
3154
|
+
...userConfig,
|
|
3118
3155
|
...coreConfigOverrides,
|
|
3119
|
-
plugins: mergedPlugins.length > 0 ? mergedPlugins :
|
|
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) {
|