@handsontable/angular-wrapper 17.1.0 → 18.0.0-rc1
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 +0 -74
- package/fesm2022/handsontable-angular-wrapper.mjs +535 -312
- package/fesm2022/handsontable-angular-wrapper.mjs.map +1 -1
- package/lib/editor/base-editor-adapter.d.ts +6 -0
- package/lib/editor/hot-cell-editor-advanced.component.d.ts +14 -46
- package/lib/editor/hot-cell-editor-base.directive.d.ts +25 -0
- package/lib/editor/hot-cell-editor.component.d.ts +16 -55
- package/lib/editor/models/factory-editor-properties.d.ts +9 -1
- package/lib/hot-table.component.d.ts +11 -9
- package/lib/hot-table.module.d.ts +1 -1
- package/lib/models/column-settings.d.ts +6 -0
- package/lib/renderer/hot-cell-renderer-advanced.component.d.ts +5 -22
- package/lib/renderer/hot-cell-renderer-base.directive.d.ts +23 -0
- package/lib/renderer/hot-cell-renderer.component.d.ts +6 -22
- package/lib/renderer/hot-dynamic-renderer-component.service.d.ts +90 -1
- package/lib/services/hot-settings-resolver.service.d.ts +29 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { ViewContainerRef, ViewChild, Input, ChangeDetectionStrategy, Component, createComponent, EventEmitter, Output, HostBinding,
|
|
2
|
+
import { ViewContainerRef, ViewChild, Input, ChangeDetectionStrategy, Component, createComponent, Directive, EventEmitter, Output, HostBinding, Injectable, InjectionToken, Inject, inject, DestroyRef, ViewEncapsulation, NgModule } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
4
|
import Handsontable from 'handsontable/base';
|
|
5
5
|
import { take, skip } from 'rxjs/operators';
|
|
@@ -43,8 +43,8 @@ class CustomEditorPlaceholderComponent {
|
|
|
43
43
|
detachEditor() {
|
|
44
44
|
this.container.detach();
|
|
45
45
|
}
|
|
46
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
47
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.
|
|
46
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CustomEditorPlaceholderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
47
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: CustomEditorPlaceholderComponent, isStandalone: true, selector: "ng-component", inputs: { top: "top", left: "left", height: "height", width: "width", isVisible: "isVisible", placeholderCustomClass: "placeholderCustomClass", componentRef: "componentRef" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["inputPlaceholder"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: ` <div
|
|
48
48
|
[class]="placeholderCustomClass"
|
|
49
49
|
[style.display]="display"
|
|
50
50
|
[style.width.px]="width"
|
|
@@ -57,7 +57,7 @@ class CustomEditorPlaceholderComponent {
|
|
|
57
57
|
<ng-template #inputPlaceholder></ng-template>
|
|
58
58
|
</div>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
59
59
|
}
|
|
60
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
60
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CustomEditorPlaceholderComponent, decorators: [{
|
|
61
61
|
type: Component,
|
|
62
62
|
args: [{
|
|
63
63
|
template: ` <div
|
|
@@ -136,14 +136,7 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
|
|
|
136
136
|
this._isPlaceholderReady = true;
|
|
137
137
|
}
|
|
138
138
|
this._componentRef = columnMeta._editorComponentReference;
|
|
139
|
-
|
|
140
|
-
this._finishEditSubscription.unsubscribe();
|
|
141
|
-
this._finishEditSubscription = undefined;
|
|
142
|
-
}
|
|
143
|
-
if (this._cancelEditSubscription) {
|
|
144
|
-
this._cancelEditSubscription.unsubscribe();
|
|
145
|
-
this._cancelEditSubscription = undefined;
|
|
146
|
-
}
|
|
139
|
+
this.cleanupSubscriptions();
|
|
147
140
|
this._finishEditSubscription = this._componentRef.instance.finishEdit
|
|
148
141
|
.pipe(take(1))
|
|
149
142
|
.subscribe(() => {
|
|
@@ -162,23 +155,23 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
|
|
|
162
155
|
close() {
|
|
163
156
|
if (this.isOpened()) {
|
|
164
157
|
this.resetEditorState();
|
|
165
|
-
this._editorPlaceHolderRef
|
|
166
|
-
this._editorPlaceHolderRef
|
|
167
|
-
this._componentRef
|
|
158
|
+
this._editorPlaceHolderRef?.changeDetectorRef.detectChanges();
|
|
159
|
+
this._editorPlaceHolderRef?.instance.detachEditor();
|
|
160
|
+
this._componentRef?.instance.onClose();
|
|
168
161
|
}
|
|
169
162
|
}
|
|
170
163
|
/**
|
|
171
164
|
* Focuses the editor. This event is triggered by Handsontable.
|
|
172
165
|
*/
|
|
173
166
|
focus() {
|
|
174
|
-
this._componentRef
|
|
167
|
+
this._componentRef?.instance.onFocus();
|
|
175
168
|
}
|
|
176
169
|
/**
|
|
177
170
|
* Gets the value from the editor.
|
|
178
171
|
* @returns The value from the editor.
|
|
179
172
|
*/
|
|
180
173
|
getValue() {
|
|
181
|
-
return this._componentRef
|
|
174
|
+
return this._componentRef?.instance?.getValue();
|
|
182
175
|
}
|
|
183
176
|
/**
|
|
184
177
|
* Opens the editor. This event is triggered by Handsontable.
|
|
@@ -190,20 +183,23 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
|
|
|
190
183
|
open(event) {
|
|
191
184
|
this.hot.getShortcutManager().setActiveContextName('editor');
|
|
192
185
|
this.applyPropsToEditor();
|
|
193
|
-
this._componentRef
|
|
186
|
+
this._componentRef?.instance.onOpen(event);
|
|
194
187
|
}
|
|
195
188
|
/**
|
|
196
189
|
* Sets the value for the custom editor.
|
|
197
190
|
* @param newValue The value to set.
|
|
198
191
|
*/
|
|
199
192
|
setValue(newValue) {
|
|
200
|
-
this._componentRef
|
|
201
|
-
this._componentRef
|
|
193
|
+
this._componentRef?.instance?.setValue(newValue);
|
|
194
|
+
this._componentRef?.changeDetectorRef.detectChanges();
|
|
202
195
|
}
|
|
203
196
|
/**
|
|
204
197
|
* Applies properties to the custom editor and editor placeholder.
|
|
205
198
|
*/
|
|
206
199
|
applyPropsToEditor() {
|
|
200
|
+
if (!this._componentRef || !this._editorPlaceHolderRef) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
207
203
|
const rect = this.getEditedCellRect();
|
|
208
204
|
if (!this.isInFullEditMode()) {
|
|
209
205
|
this._componentRef.instance.setValue(null);
|
|
@@ -253,14 +249,33 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
|
|
|
253
249
|
* Handles the after destroy event.
|
|
254
250
|
*/
|
|
255
251
|
onAfterDestroy() {
|
|
252
|
+
this.cleanupSubscriptions();
|
|
256
253
|
this._editorPlaceHolderRef?.destroy();
|
|
257
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Unsubscribes the finish/cancel edit subscriptions if they are still active.
|
|
257
|
+
* Without this, destroying the table while an editor was prepared but never
|
|
258
|
+
* finished/cancelled leaves the `take(1)` subscriptions hanging (they never emit).
|
|
259
|
+
*/
|
|
260
|
+
cleanupSubscriptions() {
|
|
261
|
+
if (this._finishEditSubscription) {
|
|
262
|
+
this._finishEditSubscription.unsubscribe();
|
|
263
|
+
this._finishEditSubscription = undefined;
|
|
264
|
+
}
|
|
265
|
+
if (this._cancelEditSubscription) {
|
|
266
|
+
this._cancelEditSubscription.unsubscribe();
|
|
267
|
+
this._cancelEditSubscription = undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
258
270
|
/**
|
|
259
271
|
* Resets the editor placeholder state.
|
|
260
272
|
* We need to reset the editor placeholder state because we use it
|
|
261
273
|
* to store multiple references to the custom editor.
|
|
262
274
|
*/
|
|
263
275
|
resetEditorState() {
|
|
276
|
+
if (!this._editorPlaceHolderRef) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
264
279
|
this._editorPlaceHolderRef.setInput('top', undefined);
|
|
265
280
|
this._editorPlaceHolderRef.setInput('left', undefined);
|
|
266
281
|
this._editorPlaceHolderRef.setInput('height', undefined);
|
|
@@ -271,44 +286,28 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
|
|
|
271
286
|
}
|
|
272
287
|
|
|
273
288
|
/**
|
|
274
|
-
*
|
|
289
|
+
* Shared base directive for HotCellRendererComponent and HotCellRendererAdvancedComponent.
|
|
290
|
+
* Holds all @Input() properties and getProps() that both renderer variants share.
|
|
275
291
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* @template TValue - The type of the component renderer.
|
|
292
|
+
* @template TValue - The type of the rendered cell value.
|
|
279
293
|
* @template TProps - The type of additional renderer properties.
|
|
280
294
|
*/
|
|
281
|
-
class
|
|
282
|
-
static RENDERER_MARKER = Symbol('HotCellRendererComponent');
|
|
295
|
+
class HotCellRendererBase {
|
|
283
296
|
value = '';
|
|
284
297
|
instance;
|
|
285
298
|
td;
|
|
286
299
|
row;
|
|
287
300
|
col;
|
|
288
301
|
prop;
|
|
289
|
-
/**
|
|
290
|
-
* The cell properties provided by Handsontable, extended with optional renderer-specific properties.
|
|
291
|
-
*/
|
|
292
302
|
cellProperties;
|
|
293
|
-
/**
|
|
294
|
-
* Retrieves the renderer-specific properties from the cell properties.
|
|
295
|
-
*
|
|
296
|
-
* @returns The additional properties for the renderer.
|
|
297
|
-
*/
|
|
298
303
|
getProps() {
|
|
299
304
|
return this.cellProperties?.rendererProps ?? {};
|
|
300
305
|
}
|
|
301
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
302
|
-
static
|
|
306
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
307
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.25", type: HotCellRendererBase, isStandalone: true, inputs: { value: "value", instance: "instance", td: "td", row: "row", col: "col", prop: "prop", cellProperties: "cellProperties" }, ngImport: i0 });
|
|
303
308
|
}
|
|
304
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
305
|
-
type:
|
|
306
|
-
args: [{
|
|
307
|
-
selector: 'hot-cell-renderer',
|
|
308
|
-
template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,
|
|
309
|
-
standalone: true,
|
|
310
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
311
|
-
}]
|
|
309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererBase, decorators: [{
|
|
310
|
+
type: Directive
|
|
312
311
|
}], propDecorators: { value: [{
|
|
313
312
|
type: Input
|
|
314
313
|
}], instance: [{
|
|
@@ -326,77 +325,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.22", ngImpo
|
|
|
326
325
|
}] } });
|
|
327
326
|
|
|
328
327
|
/**
|
|
329
|
-
* Abstract
|
|
328
|
+
* Abstract base component for creating custom cell renderer components for Handsontable.
|
|
329
|
+
*
|
|
330
|
+
* Extend this component and provide your own template to implement a custom renderer.
|
|
331
|
+
* Value type is limited to primitives (`string | number | boolean`).
|
|
332
|
+
* For object and array values use {@link HotCellRendererAdvancedComponent}.
|
|
333
|
+
*
|
|
334
|
+
* @template TValue - The type of the component renderer.
|
|
335
|
+
* @template TProps - The type of additional renderer properties.
|
|
330
336
|
*/
|
|
331
|
-
class
|
|
332
|
-
static
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
class HotCellRendererComponent extends HotCellRendererBase {
|
|
338
|
+
static RENDERER_MARKER = Symbol('HotCellRendererComponent');
|
|
339
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
340
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HotCellRendererComponent, isStandalone: true, selector: "hot-cell-renderer", usesInheritance: true, ngImport: i0, template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
341
|
+
}
|
|
342
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererComponent, decorators: [{
|
|
343
|
+
type: Component,
|
|
344
|
+
args: [{
|
|
345
|
+
selector: 'hot-cell-renderer',
|
|
346
|
+
template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,
|
|
347
|
+
standalone: true,
|
|
348
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
349
|
+
}]
|
|
350
|
+
}] });
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Shared base directive for HotCellEditorComponent and HotCellEditorAdvancedComponent.
|
|
354
|
+
* Holds all @Input(), @Output() and @HostBinding() declarations that both editor variants share.
|
|
355
|
+
*
|
|
356
|
+
* @template T - The type of the edited cell value.
|
|
357
|
+
*/
|
|
358
|
+
class HotCellEditorBase {
|
|
340
359
|
heightFitParentContainer = 100;
|
|
341
|
-
/** The width of the editor as a percentage of the parent container. */
|
|
342
360
|
widthFitParentContainer = 100;
|
|
343
|
-
/** The row index of the cell being edited. */
|
|
344
361
|
row;
|
|
345
|
-
/** The column index of the cell being edited. */
|
|
346
362
|
column;
|
|
347
|
-
/** The property name of the cell being edited. */
|
|
348
363
|
prop;
|
|
349
|
-
/** The original value of the cell being edited. */
|
|
350
364
|
originalValue;
|
|
351
|
-
/** The cell properties of the cell being edited. */
|
|
352
365
|
cellProperties;
|
|
353
|
-
/** Event emitted when the edit is finished.
|
|
354
|
-
* The data will be saved to the model.
|
|
355
|
-
*/
|
|
356
366
|
finishEdit = new EventEmitter();
|
|
357
|
-
/** Event emitted when the edit is canceled.
|
|
358
|
-
* The entered data will be reverted to the original value.
|
|
359
|
-
*/
|
|
360
367
|
cancelEdit = new EventEmitter();
|
|
361
|
-
|
|
362
|
-
_value;
|
|
363
|
-
/** Event triggered by Handsontable on closing the editor.
|
|
364
|
-
* The user can define their own actions for
|
|
365
|
-
* the custom editor to be called after the base logic. */
|
|
366
|
-
onClose() { }
|
|
367
|
-
/** Event triggered by Handsontable on open the editor.
|
|
368
|
-
* The user can define their own actions for
|
|
369
|
-
* the custom editor to be called after the base logic. */
|
|
370
|
-
onOpen(event) { }
|
|
371
|
-
/**
|
|
372
|
-
* Gets the current value of the editor.
|
|
373
|
-
* @returns The current value of the editor.
|
|
374
|
-
*/
|
|
368
|
+
value;
|
|
375
369
|
getValue() {
|
|
376
|
-
return this.
|
|
370
|
+
return this.value;
|
|
377
371
|
}
|
|
378
|
-
/**
|
|
379
|
-
* Sets the current value of the editor.
|
|
380
|
-
* @param value The value to set.
|
|
381
|
-
*/
|
|
382
372
|
setValue(value) {
|
|
383
|
-
this.
|
|
373
|
+
this.value = value;
|
|
384
374
|
}
|
|
385
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
386
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.
|
|
375
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
376
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.25", type: HotCellEditorBase, isStandalone: true, inputs: { row: "row", column: "column", prop: "prop", originalValue: "originalValue", cellProperties: "cellProperties" }, outputs: { finishEdit: "finishEdit", cancelEdit: "cancelEdit" }, host: { properties: { "style.height.%": "this.heightFitParentContainer", "style.width.%": "this.widthFitParentContainer" } }, ngImport: i0 });
|
|
387
377
|
}
|
|
388
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
378
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorBase, decorators: [{
|
|
389
379
|
type: Directive
|
|
390
|
-
}], propDecorators: {
|
|
391
|
-
type: HostBinding,
|
|
392
|
-
args: ['attr.tabindex']
|
|
393
|
-
}], dataHotInput: [{
|
|
394
|
-
type: HostBinding,
|
|
395
|
-
args: ['attr.data-hot-input']
|
|
396
|
-
}], handsontableInputClass: [{
|
|
397
|
-
type: HostBinding,
|
|
398
|
-
args: ['class.handsontableInput']
|
|
399
|
-
}], heightFitParentContainer: [{
|
|
380
|
+
}], propDecorators: { heightFitParentContainer: [{
|
|
400
381
|
type: HostBinding,
|
|
401
382
|
args: ['style.height.%']
|
|
402
383
|
}], widthFitParentContainer: [{
|
|
@@ -418,6 +399,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.22", ngImpo
|
|
|
418
399
|
type: Output
|
|
419
400
|
}] } });
|
|
420
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Abstract class representing a basic Handsontable cell editor in Angular.
|
|
404
|
+
*
|
|
405
|
+
* Extend this class and decorate the subclass with `@Component()` to implement a custom editor.
|
|
406
|
+
* Value type is limited to primitives (`string | number | boolean`).
|
|
407
|
+
* For object and array values use {@link HotCellEditorAdvancedComponent}.
|
|
408
|
+
*/
|
|
409
|
+
class HotCellEditorComponent extends HotCellEditorBase {
|
|
410
|
+
static EDITOR_MARKER = Symbol('HotCellEditorComponent');
|
|
411
|
+
/** The tabindex attribute for the editor. */
|
|
412
|
+
tabindex = -1;
|
|
413
|
+
/** The data-hot-input attribute for the editor. */
|
|
414
|
+
dataHotInput = '';
|
|
415
|
+
/** The handsontableInput class for the editor. */
|
|
416
|
+
handsontableInputClass = true;
|
|
417
|
+
/** Event triggered by Handsontable on closing the editor. */
|
|
418
|
+
onClose() { }
|
|
419
|
+
/** Event triggered by Handsontable on opening the editor. */
|
|
420
|
+
onOpen(event) { }
|
|
421
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
422
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.25", type: HotCellEditorComponent, isStandalone: true, host: { properties: { "attr.tabindex": "this.tabindex", "attr.data-hot-input": "this.dataHotInput", "class.handsontableInput": "this.handsontableInputClass" } }, usesInheritance: true, ngImport: i0 });
|
|
423
|
+
}
|
|
424
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorComponent, decorators: [{
|
|
425
|
+
type: Directive
|
|
426
|
+
}], propDecorators: { tabindex: [{
|
|
427
|
+
type: HostBinding,
|
|
428
|
+
args: ['attr.tabindex']
|
|
429
|
+
}], dataHotInput: [{
|
|
430
|
+
type: HostBinding,
|
|
431
|
+
args: ['attr.data-hot-input']
|
|
432
|
+
}], handsontableInputClass: [{
|
|
433
|
+
type: HostBinding,
|
|
434
|
+
args: ['class.handsontableInput']
|
|
435
|
+
}] } });
|
|
436
|
+
|
|
421
437
|
/**
|
|
422
438
|
* Factory function to create a custom Handsontable editor adapter for Angular components.
|
|
423
439
|
*
|
|
@@ -438,7 +454,7 @@ const FactoryEditorAdapter = (componentRef) => editorFactory({
|
|
|
438
454
|
editor._finishEditSubscription = undefined;
|
|
439
455
|
editor._cancelEditSubscription = undefined;
|
|
440
456
|
createEditorPlaceholder(editor, editor.hot._angularEnvironmentInjector);
|
|
441
|
-
editor.input = editor._editorPlaceHolderRef
|
|
457
|
+
editor.input = editor._editorPlaceHolderRef?.location.nativeElement ?? document.createElement('div');
|
|
442
458
|
editor._afterRowResizeCallback = () => {
|
|
443
459
|
if (editor.isOpened()) {
|
|
444
460
|
applyPropsToEditor(editor);
|
|
@@ -450,6 +466,7 @@ const FactoryEditorAdapter = (componentRef) => editorFactory({
|
|
|
450
466
|
}
|
|
451
467
|
};
|
|
452
468
|
editor._afterDestroyCallback = () => {
|
|
469
|
+
cleanupSubscriptions(editor);
|
|
453
470
|
if (editor._editorPlaceHolderRef) {
|
|
454
471
|
editor._editorPlaceHolderRef.destroy();
|
|
455
472
|
}
|
|
@@ -477,8 +494,8 @@ const FactoryEditorAdapter = (componentRef) => editorFactory({
|
|
|
477
494
|
onFocus: (editor) => editor._componentRef.instance.onFocus?.(editor),
|
|
478
495
|
afterClose: (editor) => {
|
|
479
496
|
resetEditorState(editor);
|
|
480
|
-
editor._editorPlaceHolderRef
|
|
481
|
-
editor._editorPlaceHolderRef
|
|
497
|
+
editor._editorPlaceHolderRef?.changeDetectorRef.detectChanges();
|
|
498
|
+
editor._editorPlaceHolderRef?.instance.detachEditor();
|
|
482
499
|
editor._componentRef.instance.afterClose?.(editor);
|
|
483
500
|
},
|
|
484
501
|
getValue: (editor) => editor._componentRef.instance.getValue(),
|
|
@@ -557,35 +574,18 @@ function cleanupSubscriptions(editor) {
|
|
|
557
574
|
/**
|
|
558
575
|
* Abstract base component for creating advanced custom cell renderer components for Handsontable.
|
|
559
576
|
*
|
|
560
|
-
*
|
|
577
|
+
* Extend this component and provide your own template to implement a custom renderer.
|
|
578
|
+
* Unlike {@link HotCellRendererComponent}, this variant also accepts object and array values.
|
|
561
579
|
*
|
|
562
580
|
* @template TValue - The type of the component renderer.
|
|
563
581
|
* @template TProps - The type of additional renderer properties.
|
|
564
582
|
*/
|
|
565
|
-
class HotCellRendererAdvancedComponent {
|
|
583
|
+
class HotCellRendererAdvancedComponent extends HotCellRendererBase {
|
|
566
584
|
static RENDERER_MARKER = Symbol('HotCellRendererAdvancedComponent');
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
td;
|
|
570
|
-
row;
|
|
571
|
-
col;
|
|
572
|
-
prop;
|
|
573
|
-
/**
|
|
574
|
-
* The cell properties provided by Handsontable, extended with optional renderer-specific properties.
|
|
575
|
-
*/
|
|
576
|
-
cellProperties;
|
|
577
|
-
/**
|
|
578
|
-
* Retrieves the renderer-specific properties from the cell properties.
|
|
579
|
-
*
|
|
580
|
-
* @returns The additional properties for the renderer.
|
|
581
|
-
*/
|
|
582
|
-
getProps() {
|
|
583
|
-
return this.cellProperties?.rendererProps ?? {};
|
|
584
|
-
}
|
|
585
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.22", ngImport: i0, type: HotCellRendererAdvancedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
586
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.22", type: HotCellRendererAdvancedComponent, isStandalone: true, selector: "hot-cell-renderer-advanced", inputs: { value: "value", instance: "instance", td: "td", row: "row", col: "col", prop: "prop", cellProperties: "cellProperties" }, ngImport: i0, template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
585
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererAdvancedComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
586
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HotCellRendererAdvancedComponent, isStandalone: true, selector: "hot-cell-renderer-advanced", usesInheritance: true, ngImport: i0, template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
587
587
|
}
|
|
588
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererAdvancedComponent, decorators: [{
|
|
589
589
|
type: Component,
|
|
590
590
|
args: [{
|
|
591
591
|
selector: 'hot-cell-renderer-advanced',
|
|
@@ -593,111 +593,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.22", ngImpo
|
|
|
593
593
|
standalone: true,
|
|
594
594
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
595
595
|
}]
|
|
596
|
-
}]
|
|
597
|
-
type: Input
|
|
598
|
-
}], instance: [{
|
|
599
|
-
type: Input
|
|
600
|
-
}], td: [{
|
|
601
|
-
type: Input
|
|
602
|
-
}], row: [{
|
|
603
|
-
type: Input
|
|
604
|
-
}], col: [{
|
|
605
|
-
type: Input
|
|
606
|
-
}], prop: [{
|
|
607
|
-
type: Input
|
|
608
|
-
}], cellProperties: [{
|
|
609
|
-
type: Input
|
|
610
|
-
}] } });
|
|
596
|
+
}] });
|
|
611
597
|
|
|
612
598
|
/**
|
|
613
|
-
* Abstract class representing
|
|
599
|
+
* Abstract class representing an advanced Handsontable cell editor in Angular.
|
|
600
|
+
*
|
|
601
|
+
* Extend this class and decorate the subclass with `@Component()` to implement a custom editor.
|
|
602
|
+
* Unlike {@link HotCellEditorComponent}, this variant also accepts object and array values
|
|
603
|
+
* and provides additional lifecycle hooks and positioning options.
|
|
614
604
|
*/
|
|
615
|
-
class HotCellEditorAdvancedComponent {
|
|
605
|
+
class HotCellEditorAdvancedComponent extends HotCellEditorBase {
|
|
616
606
|
static EDITOR_MARKER = Symbol('HotCellEditorAdvancedComponent');
|
|
617
|
-
/**
|
|
618
|
-
heightFitParentContainer = 100;
|
|
619
|
-
/** The width of the editor as a percentage of the parent container. */
|
|
620
|
-
widthFitParentContainer = 100;
|
|
621
|
-
/** The row index of the cell being edited. */
|
|
622
|
-
row;
|
|
623
|
-
/** The column index of the cell being edited. */
|
|
624
|
-
column;
|
|
625
|
-
/** The property name of the cell being edited. */
|
|
626
|
-
prop;
|
|
627
|
-
/** The original value of the cell being edited. */
|
|
628
|
-
originalValue;
|
|
629
|
-
/** The cell properties of the cell being edited. */
|
|
630
|
-
cellProperties;
|
|
631
|
-
/** Event emitted when the edit is finished.
|
|
632
|
-
* The data will be saved to the model.
|
|
633
|
-
*/
|
|
634
|
-
finishEdit = new EventEmitter();
|
|
635
|
-
/** Event emitted when the edit is canceled.
|
|
636
|
-
* The entered data will be reverted to the original value.
|
|
637
|
-
*/
|
|
638
|
-
cancelEdit = new EventEmitter();
|
|
639
|
-
/** The current value of the editor. */
|
|
640
|
-
value;
|
|
641
|
-
/** Event triggered by Handsontable on focus the editor.
|
|
642
|
-
* The user have to define focus logic.
|
|
643
|
-
*/
|
|
607
|
+
/** Event triggered by Handsontable on focusing the editor. Available in advanced mode. */
|
|
644
608
|
onFocus(editor) { }
|
|
645
|
-
/**
|
|
646
|
-
* Gets the current value of the editor.
|
|
647
|
-
* @returns The current value of the editor.
|
|
648
|
-
*/
|
|
649
|
-
getValue() {
|
|
650
|
-
return this.value;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Sets the current value of the editor.
|
|
654
|
-
* @param value The value to set.
|
|
655
|
-
*/
|
|
656
|
-
setValue(value) {
|
|
657
|
-
this.value = value;
|
|
658
|
-
}
|
|
659
|
-
/** The position of the editor in the DOM. Used by Handsontable API. Available in advanced mode. */
|
|
609
|
+
/** The position of the editor in the DOM. Available in advanced mode. */
|
|
660
610
|
position = 'container';
|
|
661
611
|
/** The shortcuts available for the editor. Available in advanced mode. */
|
|
662
612
|
shortcuts;
|
|
663
|
-
/** The group name for the shortcuts. Available in advanced mode
|
|
613
|
+
/** The group name for the shortcuts. Available in advanced mode. */
|
|
664
614
|
shortcutsGroup;
|
|
665
|
-
/** Configuration. Available in advanced mode. */
|
|
615
|
+
/** Configuration object. Available in advanced mode. */
|
|
666
616
|
config;
|
|
667
|
-
/** Lifecycle hook called after the editor is opened. Available in advanced mode
|
|
617
|
+
/** Lifecycle hook called after the editor is opened. Available in advanced mode. */
|
|
668
618
|
afterOpen(editor, event) { }
|
|
669
619
|
/** Lifecycle hook called after the editor is closed. Available in advanced mode. */
|
|
670
620
|
afterClose(editor) { }
|
|
671
|
-
/** Lifecycle hook called after the editor is initialized. Available in advanced mode
|
|
621
|
+
/** Lifecycle hook called after the editor is initialized. Available in advanced mode. */
|
|
672
622
|
afterInit(editor) { }
|
|
673
623
|
/** Lifecycle hook called before the editor is opened. Available in advanced mode. */
|
|
674
624
|
beforeOpen(editor, { row, col, prop, td, originalValue, cellProperties, }) { }
|
|
675
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
676
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.
|
|
625
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorAdvancedComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
626
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.25", type: HotCellEditorAdvancedComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
|
|
677
627
|
}
|
|
678
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
628
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorAdvancedComponent, decorators: [{
|
|
679
629
|
type: Directive
|
|
680
|
-
}]
|
|
681
|
-
type: HostBinding,
|
|
682
|
-
args: ['style.height.%']
|
|
683
|
-
}], widthFitParentContainer: [{
|
|
684
|
-
type: HostBinding,
|
|
685
|
-
args: ['style.width.%']
|
|
686
|
-
}], row: [{
|
|
687
|
-
type: Input
|
|
688
|
-
}], column: [{
|
|
689
|
-
type: Input
|
|
690
|
-
}], prop: [{
|
|
691
|
-
type: Input
|
|
692
|
-
}], originalValue: [{
|
|
693
|
-
type: Input
|
|
694
|
-
}], cellProperties: [{
|
|
695
|
-
type: Input
|
|
696
|
-
}], finishEdit: [{
|
|
697
|
-
type: Output
|
|
698
|
-
}], cancelEdit: [{
|
|
699
|
-
type: Output
|
|
700
|
-
}] } });
|
|
630
|
+
}] });
|
|
701
631
|
|
|
702
632
|
const INVALID_RENDERER_WARNING = 'The provided renderer component was not recognized as a valid custom renderer. ' +
|
|
703
633
|
'It must either extend HotCellRendererComponent or be a valid TemplateRef. ' +
|
|
@@ -705,6 +635,11 @@ const INVALID_RENDERER_WARNING = 'The provided renderer component was not recogn
|
|
|
705
635
|
const INVALID_ADVANCED_RENDERER_WARNING = 'The provided renderer component was not recognized as a valid custom renderer. ' +
|
|
706
636
|
'It must either extend HotCellRendererAdvancedComponent. ' +
|
|
707
637
|
'Please ensure that your custom renderer is implemented correctly and imported from the proper source.';
|
|
638
|
+
// Renderer component inputs, listed once at module scope so the per-cell render path can set them
|
|
639
|
+
// without allocating a fresh key array (via Object.keys) on every cell of every render frame.
|
|
640
|
+
const RENDERER_INPUT_KEYS = [
|
|
641
|
+
'instance', 'td', 'row', 'col', 'prop', 'value', 'cellProperties',
|
|
642
|
+
];
|
|
708
643
|
/**
|
|
709
644
|
* Type guard that checks if the given object is a TemplateRef.
|
|
710
645
|
*
|
|
@@ -712,7 +647,7 @@ const INVALID_ADVANCED_RENDERER_WARNING = 'The provided renderer component was n
|
|
|
712
647
|
* @returns True if the object is a TemplateRef; otherwise, false.
|
|
713
648
|
*/
|
|
714
649
|
function isTemplateRef(obj) {
|
|
715
|
-
return obj && typeof obj.createEmbeddedView === 'function';
|
|
650
|
+
return !!obj && typeof obj.createEmbeddedView === 'function';
|
|
716
651
|
}
|
|
717
652
|
/**
|
|
718
653
|
* Type guard to check if an object is an instance of HotCellRendererComponent.
|
|
@@ -745,6 +680,23 @@ function isAdvancedHotCellRendererComponent(obj) {
|
|
|
745
680
|
class DynamicComponentService {
|
|
746
681
|
appRef;
|
|
747
682
|
environmentInjector;
|
|
683
|
+
// Track Angular component refs and embedded views keyed by the TD element they are attached to.
|
|
684
|
+
// When a cell is re-rendered the previous component is destroyed before a new one is created.
|
|
685
|
+
_tdComponentRefs = new WeakMap();
|
|
686
|
+
_tdEmbeddedViews = new WeakMap();
|
|
687
|
+
// Per-instance registries of every renderer ref/view currently attached to the application.
|
|
688
|
+
// The WeakMaps above only allow per-TD lookup; these sets let us sweep refs whose TD was dropped
|
|
689
|
+
// from Handsontable's virtual viewport (scrolling, updateData), which would otherwise stay
|
|
690
|
+
// attached to ApplicationRef forever and leak both memory and change-detection work.
|
|
691
|
+
//
|
|
692
|
+
// The registries are scoped per Handsontable instance (not one global set) because this service
|
|
693
|
+
// is a root singleton shared by every <hot-table>. A global sweep would scan the cells of every
|
|
694
|
+
// table on the page on each `afterViewRender`, i.e. on every scroll frame of any one of them.
|
|
695
|
+
// Keying by instance bounds each sweep to the cells of the table that actually re-rendered.
|
|
696
|
+
_instanceComponentRefs = new WeakMap();
|
|
697
|
+
_instanceEmbeddedViews = new WeakMap();
|
|
698
|
+
// Instances we already wired the sweep hook into, so each instance is hooked at most once.
|
|
699
|
+
_hookedInstances = new WeakSet();
|
|
748
700
|
constructor(appRef, environmentInjector) {
|
|
749
701
|
this.appRef = appRef;
|
|
750
702
|
this.environmentInjector = environmentInjector;
|
|
@@ -759,6 +711,7 @@ class DynamicComponentService {
|
|
|
759
711
|
* @returns A renderer function that can be used in Handsontable's configuration.
|
|
760
712
|
*/
|
|
761
713
|
createRendererFromComponent(component, componentProps = {}, register = false) {
|
|
714
|
+
let registered = false;
|
|
762
715
|
return (instance, td, row, col, prop, value, cellProperties) => {
|
|
763
716
|
const properties = {
|
|
764
717
|
value,
|
|
@@ -769,24 +722,24 @@ class DynamicComponentService {
|
|
|
769
722
|
prop,
|
|
770
723
|
cellProperties,
|
|
771
724
|
};
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const rendererParameters = [instance, td, row, col, prop, value, cellProperties];
|
|
776
|
-
baseRenderer.apply(this, rendererParameters);
|
|
777
|
-
td.innerHTML = '';
|
|
725
|
+
cellProperties.rendererProps = componentProps;
|
|
726
|
+
baseRenderer.call(this, instance, td, row, col, prop, value, cellProperties);
|
|
727
|
+
this.registerSweepHook(instance);
|
|
778
728
|
if (isTemplateRef(component)) {
|
|
779
|
-
|
|
729
|
+
// Embedded views carry a per-render context, so they are always rebuilt.
|
|
730
|
+
this.replaceCellContent(instance, td);
|
|
731
|
+
const embeddedView = this.attachTemplateToElement(component, td, properties);
|
|
732
|
+
this.trackEmbeddedView(instance, td, embeddedView);
|
|
780
733
|
}
|
|
781
734
|
else if (isHotCellRendererComponent(component)) {
|
|
782
|
-
|
|
783
|
-
this.attachComponentToElement(componentRef, td);
|
|
735
|
+
this.renderComponent(td, component, properties);
|
|
784
736
|
}
|
|
785
737
|
else {
|
|
786
738
|
console.warn(INVALID_RENDERER_WARNING);
|
|
787
739
|
}
|
|
788
|
-
if (register && isHotCellRendererComponent(component)) {
|
|
789
|
-
Handsontable.renderers.registerRenderer(component.
|
|
740
|
+
if (register && !registered && isHotCellRendererComponent(component)) {
|
|
741
|
+
Handsontable.renderers.registerRenderer(component.name, component);
|
|
742
|
+
registered = true;
|
|
790
743
|
}
|
|
791
744
|
return td;
|
|
792
745
|
};
|
|
@@ -801,6 +754,7 @@ class DynamicComponentService {
|
|
|
801
754
|
* @returns A renderer function that can be used in Handsontable's configuration.
|
|
802
755
|
*/
|
|
803
756
|
createRendererWithFactory(component, componentProps = {}, register = false) {
|
|
757
|
+
let registered = false;
|
|
804
758
|
return rendererFactory(({ instance, td, row, column, prop, value, cellProperties }) => {
|
|
805
759
|
const properties = {
|
|
806
760
|
value,
|
|
@@ -811,28 +765,161 @@ class DynamicComponentService {
|
|
|
811
765
|
prop,
|
|
812
766
|
cellProperties,
|
|
813
767
|
};
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
768
|
+
cellProperties.rendererProps = componentProps;
|
|
769
|
+
// Apply the base renderer so the TD gets the same base classes/attributes as the
|
|
770
|
+
// createRendererFromComponent path (rendererFactory itself does not call it).
|
|
771
|
+
baseRenderer.call(this, instance, td, row, column, prop, value, cellProperties);
|
|
772
|
+
this.registerSweepHook(instance);
|
|
818
773
|
if (isAdvancedHotCellRendererComponent(component)) {
|
|
819
|
-
|
|
820
|
-
this.attachComponentToElement(componentRef, td);
|
|
774
|
+
this.renderComponent(td, component, properties);
|
|
821
775
|
}
|
|
822
776
|
else {
|
|
823
777
|
console.warn(INVALID_ADVANCED_RENDERER_WARNING);
|
|
824
778
|
}
|
|
825
|
-
if (register && isAdvancedHotCellRendererComponent(component)) {
|
|
826
|
-
registerRenderer(component.
|
|
779
|
+
if (register && !registered && isAdvancedHotCellRendererComponent(component)) {
|
|
780
|
+
registerRenderer(component.name, component);
|
|
781
|
+
registered = true;
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Destroys all renderer components and embedded views attached to cells within a container element.
|
|
787
|
+
* Must be called before destroying the Handsontable instance to prevent Angular component leaks.
|
|
788
|
+
*
|
|
789
|
+
* @param container - The root DOM element of the Handsontable instance.
|
|
790
|
+
* @param instance - The Handsontable instance whose registries should be torn down. When omitted
|
|
791
|
+
* (e.g. test stubs), only refs reachable through TDs still in the container are destroyed.
|
|
792
|
+
*/
|
|
793
|
+
cleanupContainer(container, instance) {
|
|
794
|
+
const compRefs = instance ? this._instanceComponentRefs.get(instance) : undefined;
|
|
795
|
+
const embViews = instance ? this._instanceEmbeddedViews.get(instance) : undefined;
|
|
796
|
+
container.querySelectorAll('td').forEach((td) => {
|
|
797
|
+
const compRef = this._tdComponentRefs.get(td);
|
|
798
|
+
if (compRef) {
|
|
799
|
+
this.destroyComponent(compRef);
|
|
800
|
+
this._tdComponentRefs.delete(td);
|
|
801
|
+
compRefs?.delete(compRef);
|
|
802
|
+
}
|
|
803
|
+
const embView = this._tdEmbeddedViews.get(td);
|
|
804
|
+
if (embView) {
|
|
805
|
+
this.destroyEmbeddedView(embView);
|
|
806
|
+
this._tdEmbeddedViews.delete(td);
|
|
807
|
+
embViews?.delete(embView);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
// The loop above only reaches TDs still present in the container. Refs for cells already
|
|
811
|
+
// dropped from the viewport (but not yet swept) would otherwise be orphaned once this
|
|
812
|
+
// instance's afterViewRender hook is gone after destroy. Tear down whatever is left and drop
|
|
813
|
+
// the per-instance registries so a repeated cleanup call is a no-op. (Entries removed in the
|
|
814
|
+
// loop above are already gone from these sets, so nothing is destroyed twice.)
|
|
815
|
+
compRefs?.forEach((ref) => this.destroyComponent(ref));
|
|
816
|
+
embViews?.forEach((view) => this.destroyEmbeddedView(view));
|
|
817
|
+
if (instance) {
|
|
818
|
+
this._instanceComponentRefs.delete(instance);
|
|
819
|
+
this._instanceEmbeddedViews.delete(instance);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Registers a one-time `afterViewRender` hook on the given Handsontable instance that sweeps
|
|
824
|
+
* renderer refs whose TD is no longer connected to the document. Handsontable recycles a pool of
|
|
825
|
+
* TD elements while virtualizing rows; cells that leave the viewport are never re-rendered, so
|
|
826
|
+
* without this sweep their Angular components stay attached to ApplicationRef and leak.
|
|
827
|
+
*
|
|
828
|
+
* Guarded against instances that do not expose `addHook` (e.g. test stubs).
|
|
829
|
+
*
|
|
830
|
+
* @param instance - The Handsontable instance whose render cycle drives the sweep.
|
|
831
|
+
*/
|
|
832
|
+
registerSweepHook(instance) {
|
|
833
|
+
if (this._hookedInstances.has(instance) || typeof instance?.addHook !== 'function') {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
this._hookedInstances.add(instance);
|
|
837
|
+
instance.addHook('afterViewRender', () => this.sweepDetachedViews(instance));
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Destroys every renderer ref/view tracked for the given instance whose root node is no longer
|
|
841
|
+
* attached to the document.
|
|
842
|
+
*
|
|
843
|
+
* @param instance - The Handsontable instance whose registries should be swept.
|
|
844
|
+
*/
|
|
845
|
+
sweepDetachedViews(instance) {
|
|
846
|
+
const compRefs = this._instanceComponentRefs.get(instance);
|
|
847
|
+
compRefs?.forEach((ref) => {
|
|
848
|
+
if (!this.isViewConnected(ref.hostView)) {
|
|
849
|
+
this.destroyComponent(ref);
|
|
850
|
+
compRefs.delete(ref);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
const embViews = this._instanceEmbeddedViews.get(instance);
|
|
854
|
+
embViews?.forEach((view) => {
|
|
855
|
+
if (!this.isViewConnected(view)) {
|
|
856
|
+
this.destroyEmbeddedView(view);
|
|
857
|
+
embViews.delete(view);
|
|
827
858
|
}
|
|
828
859
|
});
|
|
829
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* @returns True if any of the view's root nodes is still connected to the document.
|
|
863
|
+
*/
|
|
864
|
+
isViewConnected(view) {
|
|
865
|
+
return view.rootNodes.some((node) => !!node?.isConnected);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Destroys the renderer ref/view previously attached to a cell and clears the cell content,
|
|
869
|
+
* so a fresh renderer can take over the TD without leaking the old one.
|
|
870
|
+
*
|
|
871
|
+
* @param instance - The Handsontable instance owning the cell, used to update its registries.
|
|
872
|
+
* @param td - The table cell whose previous content should be torn down.
|
|
873
|
+
*/
|
|
874
|
+
replaceCellContent(instance, td) {
|
|
875
|
+
const prevRef = this._tdComponentRefs.get(td);
|
|
876
|
+
if (prevRef) {
|
|
877
|
+
this.destroyComponent(prevRef);
|
|
878
|
+
this._tdComponentRefs.delete(td);
|
|
879
|
+
this._instanceComponentRefs.get(instance)?.delete(prevRef);
|
|
880
|
+
}
|
|
881
|
+
const prevView = this._tdEmbeddedViews.get(td);
|
|
882
|
+
if (prevView) {
|
|
883
|
+
this.destroyEmbeddedView(prevView);
|
|
884
|
+
this._tdEmbeddedViews.delete(td);
|
|
885
|
+
this._instanceEmbeddedViews.get(instance)?.delete(prevView);
|
|
886
|
+
}
|
|
887
|
+
td.innerHTML = '';
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Tracks a component ref both by its TD (for fast replacement) and in the instance registry
|
|
891
|
+
* (for sweeping and full teardown).
|
|
892
|
+
*/
|
|
893
|
+
trackComponentRef(instance, td, ref) {
|
|
894
|
+
this._tdComponentRefs.set(td, ref);
|
|
895
|
+
this.registryFor(this._instanceComponentRefs, instance).add(ref);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Tracks an embedded view both by its TD (for fast replacement) and in the instance registry
|
|
899
|
+
* (for sweeping and full teardown).
|
|
900
|
+
*/
|
|
901
|
+
trackEmbeddedView(instance, td, view) {
|
|
902
|
+
this._tdEmbeddedViews.set(td, view);
|
|
903
|
+
this.registryFor(this._instanceEmbeddedViews, instance).add(view);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Returns the per-instance registry set for the given instance, creating it on first use.
|
|
907
|
+
*/
|
|
908
|
+
registryFor(registry, instance) {
|
|
909
|
+
let set = registry.get(instance);
|
|
910
|
+
if (!set) {
|
|
911
|
+
set = new Set();
|
|
912
|
+
registry.set(instance, set);
|
|
913
|
+
}
|
|
914
|
+
return set;
|
|
915
|
+
}
|
|
830
916
|
/**
|
|
831
917
|
* Attaches an embedded view created from a TemplateRef to a given DOM element.
|
|
832
918
|
*
|
|
833
919
|
* @param template - The TemplateRef to create an embedded view from.
|
|
834
920
|
* @param tdEl - The target DOM element (a table cell) to which the view will be appended.
|
|
835
921
|
* @param properties - Context object providing properties to be used within the template.
|
|
922
|
+
* @returns The created EmbeddedViewRef so the caller can track and destroy it later.
|
|
836
923
|
*/
|
|
837
924
|
attachTemplateToElement(template, tdEl, properties) {
|
|
838
925
|
const embeddedView = template.createEmbeddedView({
|
|
@@ -843,6 +930,7 @@ class DynamicComponentService {
|
|
|
843
930
|
embeddedView.rootNodes.forEach((node) => {
|
|
844
931
|
tdEl.appendChild(node);
|
|
845
932
|
});
|
|
933
|
+
return embeddedView;
|
|
846
934
|
}
|
|
847
935
|
/**
|
|
848
936
|
* Dynamically creates an Angular component of the given type.
|
|
@@ -855,18 +943,46 @@ class DynamicComponentService {
|
|
|
855
943
|
const componentRef = createComponent(component, {
|
|
856
944
|
environmentInjector: this.environmentInjector,
|
|
857
945
|
});
|
|
858
|
-
|
|
859
|
-
if (Object.prototype.hasOwnProperty.call(rendererParameters, key)) {
|
|
860
|
-
componentRef.setInput(key, rendererParameters[key]);
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
console.warn(`Input property "${key}" does not exist on component instance: ${component?.name}.`);
|
|
864
|
-
}
|
|
865
|
-
});
|
|
946
|
+
this.applyInputs(componentRef, rendererParameters);
|
|
866
947
|
componentRef.changeDetectorRef.detectChanges();
|
|
867
948
|
this.appRef.attachView(componentRef.hostView);
|
|
868
949
|
return componentRef;
|
|
869
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Renders an Angular component into the given cell, recycling the component already attached to
|
|
953
|
+
* the TD when it is of the same type. Handsontable recycles its pool of TD elements heavily while
|
|
954
|
+
* virtualizing rows, so recreating an Angular component on every re-render would cause needless
|
|
955
|
+
* teardown/instantiation churn and GC pressure. When the type matches we only refresh the inputs.
|
|
956
|
+
*
|
|
957
|
+
* @param td - The target table cell.
|
|
958
|
+
* @param component - The renderer component type to render.
|
|
959
|
+
* @param properties - The renderer parameters to feed as component inputs.
|
|
960
|
+
*/
|
|
961
|
+
renderComponent(td, component, properties) {
|
|
962
|
+
const prevRef = this._tdComponentRefs.get(td);
|
|
963
|
+
if (prevRef &&
|
|
964
|
+
prevRef.componentType === component &&
|
|
965
|
+
this.isViewConnected(prevRef.hostView)) {
|
|
966
|
+
this.applyInputs(prevRef, properties);
|
|
967
|
+
prevRef.changeDetectorRef.detectChanges();
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
this.replaceCellContent(properties.instance, td);
|
|
971
|
+
const componentRef = this.createComponent(component, properties);
|
|
972
|
+
this.attachComponentToElement(componentRef, td);
|
|
973
|
+
this.trackComponentRef(properties.instance, td, componentRef);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Assigns every renderer parameter as an input on the given component ref.
|
|
977
|
+
*
|
|
978
|
+
* @param componentRef - The component ref whose inputs should be set.
|
|
979
|
+
* @param rendererParameters - The renderer parameters to assign.
|
|
980
|
+
*/
|
|
981
|
+
applyInputs(componentRef, rendererParameters) {
|
|
982
|
+
RENDERER_INPUT_KEYS.forEach((key) => {
|
|
983
|
+
componentRef.setInput(key, rendererParameters[key]);
|
|
984
|
+
});
|
|
985
|
+
}
|
|
870
986
|
/**
|
|
871
987
|
* Attaches a dynamically created component's view to a specified DOM container element.
|
|
872
988
|
*
|
|
@@ -874,22 +990,44 @@ class DynamicComponentService {
|
|
|
874
990
|
* @param container - The target DOM element to which the component's root node will be appended.
|
|
875
991
|
*/
|
|
876
992
|
attachComponentToElement(componentRef, container) {
|
|
877
|
-
|
|
878
|
-
|
|
993
|
+
componentRef.hostView.rootNodes.forEach((node) => {
|
|
994
|
+
container.appendChild(node);
|
|
995
|
+
});
|
|
879
996
|
}
|
|
880
997
|
/**
|
|
881
998
|
* Destroys a dynamically created component and detaches its view from the Angular application.
|
|
882
999
|
*
|
|
1000
|
+
* Idempotent: a TD recycled after `sweepDetachedViews` already destroyed its ref still maps to that
|
|
1001
|
+
* stale ref in `_tdComponentRefs`, so the next render path may reach this with an already-destroyed
|
|
1002
|
+
* ref. Guarding on `destroyed` skips the redundant detach/destroy instead of relying on Angular's
|
|
1003
|
+
* internal no-op behaviour.
|
|
1004
|
+
*
|
|
883
1005
|
* @param componentRef - The reference to the component to be destroyed.
|
|
884
1006
|
*/
|
|
885
1007
|
destroyComponent(componentRef) {
|
|
886
|
-
|
|
1008
|
+
const hostView = componentRef.hostView;
|
|
1009
|
+
if (hostView.destroyed) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
this.appRef.detachView(hostView);
|
|
887
1013
|
componentRef.destroy();
|
|
888
1014
|
}
|
|
889
|
-
|
|
890
|
-
|
|
1015
|
+
/**
|
|
1016
|
+
* Destroys an embedded view. Idempotent for the same reason as {@link destroyComponent}: a recycled
|
|
1017
|
+
* TD can still map to an already-destroyed view in `_tdEmbeddedViews`.
|
|
1018
|
+
*
|
|
1019
|
+
* @param view - The embedded view to destroy.
|
|
1020
|
+
*/
|
|
1021
|
+
destroyEmbeddedView(view) {
|
|
1022
|
+
if (view.destroyed) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
view.destroy();
|
|
1026
|
+
}
|
|
1027
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicComponentService, deps: [{ token: i0.ApplicationRef }, { token: i0.EnvironmentInjector }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1028
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicComponentService, providedIn: 'root' });
|
|
891
1029
|
}
|
|
892
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
1030
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicComponentService, decorators: [{
|
|
893
1031
|
type: Injectable,
|
|
894
1032
|
args: [{
|
|
895
1033
|
providedIn: 'root',
|
|
@@ -897,6 +1035,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.22", ngImpo
|
|
|
897
1035
|
}], ctorParameters: () => [{ type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }] });
|
|
898
1036
|
|
|
899
1037
|
const AVAILABLE_HOOKS_SET = new Set(Handsontable.hooks.getRegistered());
|
|
1038
|
+
const HOT_ZONE_WRAPPED = Symbol('hotZoneWrapped');
|
|
900
1039
|
/**
|
|
901
1040
|
* Service to resolve and apply custom settings for Handsontable settings object.
|
|
902
1041
|
*/
|
|
@@ -912,15 +1051,26 @@ class HotSettingsResolver {
|
|
|
912
1051
|
/**
|
|
913
1052
|
* Applies custom settings to the provided GridSettings.
|
|
914
1053
|
* @param settings The original grid settings.
|
|
1054
|
+
* @param previousColumns The previously resolved columns (from the prior settings cycle). When
|
|
1055
|
+
* supplied, an editor component already created for a column whose editor type is unchanged is
|
|
1056
|
+
* recycled instead of being recreated, avoiding needless Angular component teardown/rebuild.
|
|
915
1057
|
* @returns The merged grid settings with custom settings applied.
|
|
916
1058
|
*/
|
|
917
|
-
applyCustomSettings(settings) {
|
|
918
|
-
|
|
1059
|
+
applyCustomSettings(settings, previousColumns) {
|
|
1060
|
+
// Shallow-clone the user settings (and each column) before mutating. Otherwise we would
|
|
1061
|
+
// write generated renderers/editors and `_editorComponentReference` straight onto the
|
|
1062
|
+
// caller's objects. When the same settings/columns are shared across two <hot-table>
|
|
1063
|
+
// instances, the second resolution would overwrite the first instance's editor refs,
|
|
1064
|
+
// leaking them and cross-wiring a single editor component between tables.
|
|
1065
|
+
const mergedSettings = { ...settings };
|
|
1066
|
+
if (Array.isArray(mergedSettings.columns)) {
|
|
1067
|
+
mergedSettings.columns = mergedSettings.columns.map((column) => ({ ...column }));
|
|
1068
|
+
}
|
|
919
1069
|
this.updateColumnRendererForGivenCustomRenderer(mergedSettings);
|
|
920
|
-
this.updateColumnEditorForGivenCustomEditor(mergedSettings);
|
|
1070
|
+
this.updateColumnEditorForGivenCustomEditor(mergedSettings, previousColumns);
|
|
921
1071
|
this.updateColumnValidatorForGivenCustomValidator(mergedSettings);
|
|
922
1072
|
this.wrapHooksInNgZone(mergedSettings);
|
|
923
|
-
return mergedSettings
|
|
1073
|
+
return mergedSettings;
|
|
924
1074
|
}
|
|
925
1075
|
/**
|
|
926
1076
|
* Ensures that hook callbacks in the provided grid settings run inside Angular's zone.
|
|
@@ -929,12 +1079,18 @@ class HotSettingsResolver {
|
|
|
929
1079
|
*/
|
|
930
1080
|
wrapHooksInNgZone(settings) {
|
|
931
1081
|
const ngZone = this.ngZone;
|
|
932
|
-
|
|
1082
|
+
// Iterate only the keys actually present in settings instead of all ~100 registered HOT hooks.
|
|
1083
|
+
Object.keys(settings).forEach((key) => {
|
|
1084
|
+
if (!AVAILABLE_HOOKS_SET.has(key)) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
933
1087
|
const option = settings[key];
|
|
934
|
-
if (typeof option === 'function') {
|
|
935
|
-
|
|
1088
|
+
if (typeof option === 'function' && !option[HOT_ZONE_WRAPPED]) {
|
|
1089
|
+
const wrapped = function (...args) {
|
|
936
1090
|
return ngZone.run(() => option.apply(this, args));
|
|
937
1091
|
};
|
|
1092
|
+
wrapped[HOT_ZONE_WRAPPED] = true;
|
|
1093
|
+
settings[key] = wrapped;
|
|
938
1094
|
}
|
|
939
1095
|
});
|
|
940
1096
|
}
|
|
@@ -961,31 +1117,73 @@ class HotSettingsResolver {
|
|
|
961
1117
|
}
|
|
962
1118
|
/**
|
|
963
1119
|
* Updates the column editor for columns with a custom editor.
|
|
1120
|
+
*
|
|
1121
|
+
* Iterates by original column index (not a filtered subset) so each column can be matched against
|
|
1122
|
+
* the column at the same index in `previousColumns` for editor-component recycling.
|
|
1123
|
+
*
|
|
964
1124
|
* @param mergedSettings The merged grid settings.
|
|
1125
|
+
* @param previousColumns The previously resolved columns, used to recycle editor components.
|
|
965
1126
|
*/
|
|
966
|
-
updateColumnEditorForGivenCustomEditor(mergedSettings) {
|
|
1127
|
+
updateColumnEditorForGivenCustomEditor(mergedSettings, previousColumns) {
|
|
967
1128
|
if (!Array.isArray(mergedSettings?.columns)) {
|
|
968
1129
|
return;
|
|
969
1130
|
}
|
|
970
|
-
mergedSettings
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
if (
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1131
|
+
mergedSettings.columns.forEach((cellSettings, index) => {
|
|
1132
|
+
const isAdvanced = this.isAdvancedEditorComponentRefType(cellSettings.editor);
|
|
1133
|
+
const isBasic = this.isEditorComponentRefType(cellSettings.editor);
|
|
1134
|
+
if (!isAdvanced && !isBasic) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const editorType = cellSettings.editor;
|
|
1138
|
+
const reusableRef = this.reusableEditorRef(previousColumns?.[index], cellSettings, editorType);
|
|
1139
|
+
const internalSettings = cellSettings;
|
|
1140
|
+
// Recycle the editor component from the previous settings cycle when the same editor type
|
|
1141
|
+
// sits at the same column index AND the same logical column (by `data`) still occupies it.
|
|
1142
|
+
// Recreating it on every settings change would tear down and rebuild an Angular component
|
|
1143
|
+
// (and its DOM/internal state) for no reason. The reused ref is carried into the new column;
|
|
1144
|
+
// HotTableComponent.ngOnChanges detects it by identity and skips destroying it.
|
|
1145
|
+
const component = reusableRef ?? createComponent(editorType, {
|
|
1146
|
+
environmentInjector: this.environmentInjector,
|
|
1147
|
+
});
|
|
1148
|
+
internalSettings._editorComponentReference = component;
|
|
1149
|
+
if (isAdvanced) {
|
|
977
1150
|
cellSettings.editor = FactoryEditorAdapter(component);
|
|
978
1151
|
}
|
|
979
1152
|
else {
|
|
980
|
-
|
|
981
|
-
environmentInjector: this.environmentInjector,
|
|
982
|
-
});
|
|
983
|
-
cellSettings['_editorComponentReference'] = component;
|
|
984
|
-
cellSettings['_environmentInjector'] = this.environmentInjector;
|
|
1153
|
+
internalSettings._environmentInjector = this.environmentInjector;
|
|
985
1154
|
cellSettings.editor = BaseEditorAdapter;
|
|
986
1155
|
}
|
|
987
1156
|
});
|
|
988
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Returns the previous column's editor component ref when it can be reused for the new column, or
|
|
1160
|
+
* `undefined` to signal a fresh component is needed.
|
|
1161
|
+
*
|
|
1162
|
+
* A ref is only recycled when, at the same index, both the editor component type AND the logical
|
|
1163
|
+
* column identity (its `data` binding) are unchanged. The component-type check alone would already
|
|
1164
|
+
* be functionally safe — a Handsontable editor is not per-cell rendered state but a single
|
|
1165
|
+
* on-demand component that `BaseEditorAdapter`/`FactoryEditorAdapter` re-prepare on every edit
|
|
1166
|
+
* (`prepare()` re-reads the ref from the *current* column meta and `applyPropsToEditor()` re-applies
|
|
1167
|
+
* the full cell context on each `open()`). The extra `data` check is a defensive guard: when columns
|
|
1168
|
+
* are reordered/shortened so a *different* logical column lands on an index, we build a fresh editor
|
|
1169
|
+
* rather than carry the previous column's instance over, so no custom editor that caches
|
|
1170
|
+
* column-specific config at construction can leak stale state into the new cell.
|
|
1171
|
+
*
|
|
1172
|
+
* @param previousColumn The column at the same index in the previous settings cycle.
|
|
1173
|
+
* @param currentColumn The column now occupying this index.
|
|
1174
|
+
* @param editorType The editor component type requested for the new column.
|
|
1175
|
+
*/
|
|
1176
|
+
reusableEditorRef(previousColumn, currentColumn, editorType) {
|
|
1177
|
+
const previousRef = previousColumn?._editorComponentReference;
|
|
1178
|
+
if (!previousRef || previousRef.componentType !== editorType) {
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
// Same logical column still occupies this index. Columns without a `data` binding are identified
|
|
1182
|
+
// purely by position, so two `undefined` data values compare equal and recycle as before.
|
|
1183
|
+
const sameLogicalColumn = previousColumn?.data ===
|
|
1184
|
+
currentColumn.data;
|
|
1185
|
+
return sameLogicalColumn ? previousRef : undefined;
|
|
1186
|
+
}
|
|
989
1187
|
/**
|
|
990
1188
|
* Updates the column validator for columns with a custom validator.
|
|
991
1189
|
* @param mergedSettings The merged grid settings.
|
|
@@ -1038,10 +1236,10 @@ class HotSettingsResolver {
|
|
|
1038
1236
|
this.isTemplateRef(renderer) ||
|
|
1039
1237
|
this.isAdvancedRendererComponentRefType(renderer);
|
|
1040
1238
|
}
|
|
1041
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
1042
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.
|
|
1239
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotSettingsResolver, deps: [{ token: DynamicComponentService }, { token: i0.EnvironmentInjector }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1240
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotSettingsResolver });
|
|
1043
1241
|
}
|
|
1044
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
1242
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotSettingsResolver, decorators: [{
|
|
1045
1243
|
type: Injectable
|
|
1046
1244
|
}], ctorParameters: () => [{ type: DynamicComponentService }, { type: i0.EnvironmentInjector }, { type: i0.NgZone }] });
|
|
1047
1245
|
|
|
@@ -1134,10 +1332,10 @@ class HotGlobalConfigService {
|
|
|
1134
1332
|
resetConfig() {
|
|
1135
1333
|
this.configSubject.next({ ...this.defaultConfig });
|
|
1136
1334
|
}
|
|
1137
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
1138
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.
|
|
1335
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotGlobalConfigService, deps: [{ token: HOT_GLOBAL_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1336
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotGlobalConfigService, providedIn: 'root' });
|
|
1139
1337
|
}
|
|
1140
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
1338
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotGlobalConfigService, decorators: [{
|
|
1141
1339
|
type: Injectable,
|
|
1142
1340
|
args: [{
|
|
1143
1341
|
providedIn: 'root',
|
|
@@ -1147,37 +1345,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.22", ngImpo
|
|
|
1147
1345
|
args: [HOT_GLOBAL_CONFIG]
|
|
1148
1346
|
}] }] });
|
|
1149
1347
|
|
|
1150
|
-
const HOT_DESTROYED_WARNING = 'The Handsontable instance bound to this component was destroyed and cannot be used properly.';
|
|
1348
|
+
const HOT_DESTROYED_WARNING = 'The Handsontable instance bound to this component was destroyed and cannot be' + ' used properly.';
|
|
1151
1349
|
class HotTableComponent {
|
|
1152
1350
|
_hotSettingsResolver;
|
|
1153
1351
|
_hotConfig;
|
|
1154
1352
|
ngZone;
|
|
1155
1353
|
environmentInjector;
|
|
1156
|
-
|
|
1354
|
+
_dynamicComponentService;
|
|
1355
|
+
// component inputs
|
|
1157
1356
|
/** The data for the Handsontable instance. */
|
|
1158
1357
|
data = null;
|
|
1159
1358
|
/** The settings for the Handsontable instance. */
|
|
1160
1359
|
settings = {};
|
|
1360
|
+
/** The container element for the Handsontable instance. */
|
|
1161
1361
|
container;
|
|
1162
1362
|
/** The Handsontable instance. */
|
|
1163
1363
|
__hotInstance = null;
|
|
1164
|
-
|
|
1364
|
+
_destroyRef = inject(DestroyRef);
|
|
1365
|
+
constructor(_hotSettingsResolver, _hotConfig, ngZone, environmentInjector, _dynamicComponentService) {
|
|
1165
1366
|
this._hotSettingsResolver = _hotSettingsResolver;
|
|
1166
1367
|
this._hotConfig = _hotConfig;
|
|
1167
1368
|
this.ngZone = ngZone;
|
|
1168
1369
|
this.environmentInjector = environmentInjector;
|
|
1169
|
-
this.
|
|
1370
|
+
this._dynamicComponentService = _dynamicComponentService;
|
|
1170
1371
|
}
|
|
1171
1372
|
/**
|
|
1172
1373
|
* Gets the Handsontable instance.
|
|
1173
1374
|
* @returns The Handsontable instance or `null` if it's not yet been created or has been destroyed.
|
|
1174
1375
|
*/
|
|
1175
1376
|
get hotInstance() {
|
|
1176
|
-
if (this.__hotInstance
|
|
1377
|
+
if (!this.__hotInstance || !this.__hotInstance.isDestroyed) {
|
|
1378
|
+
// Will return the Handsontable instance or `null` if it's not yet been created.
|
|
1379
|
+
return this.__hotInstance;
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1177
1382
|
console.warn(HOT_DESTROYED_WARNING);
|
|
1178
1383
|
return null;
|
|
1179
1384
|
}
|
|
1180
|
-
return this.__hotInstance;
|
|
1181
1385
|
}
|
|
1182
1386
|
/**
|
|
1183
1387
|
* Sets the Handsontable instance.
|
|
@@ -1193,13 +1397,13 @@ class HotTableComponent {
|
|
|
1193
1397
|
ngAfterViewInit() {
|
|
1194
1398
|
let options = this._hotSettingsResolver.applyCustomSettings(this.settings);
|
|
1195
1399
|
const negotiatedSettings = this.getNegotiatedSettings(options);
|
|
1196
|
-
options = { ...options, ...negotiatedSettings, data: this.data };
|
|
1400
|
+
options = { ...options, ...negotiatedSettings, ...(this.data != null ? { data: this.data } : {}) };
|
|
1197
1401
|
this.ngZone.runOutsideAngular(() => {
|
|
1198
1402
|
this.hotInstance = new Handsontable.Core(this.container.nativeElement, options);
|
|
1199
1403
|
this.hotInstance._angularEnvironmentInjector = this.environmentInjector;
|
|
1200
1404
|
this.hotInstance.init();
|
|
1201
1405
|
});
|
|
1202
|
-
this._hotConfig.config$.pipe(skip(1), takeUntilDestroyed(this.
|
|
1406
|
+
this._hotConfig.config$.pipe(skip(1), takeUntilDestroyed(this._destroyRef)).subscribe((config) => {
|
|
1203
1407
|
if (this.hotInstance) {
|
|
1204
1408
|
const negotiatedSettings = this.getNegotiatedSettings(this.settings);
|
|
1205
1409
|
this.updateHotTable(negotiatedSettings);
|
|
@@ -1211,37 +1415,56 @@ class HotTableComponent {
|
|
|
1211
1415
|
return;
|
|
1212
1416
|
}
|
|
1213
1417
|
if (changes.settings && !changes.settings.firstChange) {
|
|
1214
|
-
|
|
1215
|
-
const
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1418
|
+
// Capture old editor refs before applying new settings so HOT can close any active editor first.
|
|
1419
|
+
const prevColumns = this.__hotInstance?.getSettings().columns;
|
|
1420
|
+
const prevColumnsArray = Array.isArray(prevColumns) ? prevColumns : undefined;
|
|
1421
|
+
// Pass the previous columns so unchanged editor types recycle their existing component
|
|
1422
|
+
// instead of creating a fresh one on every settings change.
|
|
1423
|
+
const newOptions = this._hotSettingsResolver.applyCustomSettings(changes.settings.currentValue, prevColumnsArray);
|
|
1424
|
+
// updateHotTable closes any active editor via HOT.updateSettings before we destroy old refs.
|
|
1425
|
+
this.updateHotTable(newOptions);
|
|
1426
|
+
// Only destroy old editor refs when new settings actually replace columns.
|
|
1427
|
+
// If newOptions has no columns, HOT keeps the old column objects active — destroying
|
|
1428
|
+
// their refs would crash FactoryEditorAdapter / BaseEditorAdapter on next edit.
|
|
1429
|
+
if (prevColumnsArray && Array.isArray(newOptions.columns)) {
|
|
1430
|
+
// Refs recycled into the new columns must survive — destroy only the ones left behind.
|
|
1431
|
+
const reusedRefs = new Set(newOptions.columns
|
|
1432
|
+
.map((column) => column._editorComponentReference)
|
|
1433
|
+
.filter((ref) => !!ref));
|
|
1434
|
+
prevColumnsArray.forEach((column) => {
|
|
1435
|
+
if (column._editorComponentReference && !reusedRefs.has(column._editorComponentReference)) {
|
|
1436
|
+
column._editorComponentReference.destroy();
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1219
1440
|
}
|
|
1220
1441
|
if (changes.data && !changes.data.firstChange) {
|
|
1221
|
-
this.
|
|
1222
|
-
this.hotInstance.updateData(changes.data.currentValue);
|
|
1223
|
-
});
|
|
1442
|
+
this.hotInstance?.updateData(changes.data.currentValue);
|
|
1224
1443
|
}
|
|
1225
1444
|
}
|
|
1226
1445
|
/**
|
|
1227
1446
|
* Destroys the Handsontable instance and clears the columns from custom editors.
|
|
1228
1447
|
*/
|
|
1229
1448
|
ngOnDestroy() {
|
|
1230
|
-
if (!this.hotInstance) {
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
this.destroyEditorComponentRefs();
|
|
1234
1449
|
this.ngZone.runOutsideAngular(() => {
|
|
1235
|
-
this.
|
|
1236
|
-
|
|
1450
|
+
if (!this.__hotInstance || this.__hotInstance.isDestroyed) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
// Destroy renderer Angular components attached to table cells before HOT removes the DOM.
|
|
1454
|
+
if (this.container) {
|
|
1455
|
+
this._dynamicComponentService.cleanupContainer(this.container.nativeElement, this.__hotInstance);
|
|
1456
|
+
}
|
|
1457
|
+
const columns = this.__hotInstance.getSettings().columns;
|
|
1458
|
+
if (columns && Array.isArray(columns)) {
|
|
1459
|
+
columns.forEach((column) => {
|
|
1460
|
+
if (column._editorComponentReference) {
|
|
1461
|
+
column._editorComponentReference.destroy();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
this.__hotInstance.destroy();
|
|
1237
1466
|
});
|
|
1238
1467
|
}
|
|
1239
|
-
destroyEditorComponentRefs() {
|
|
1240
|
-
const columns = this.hotInstance.getSettings().columns;
|
|
1241
|
-
if (Array.isArray(columns)) {
|
|
1242
|
-
columns.forEach((column) => column._editorComponentReference?.destroy());
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
1468
|
/**
|
|
1246
1469
|
* Updates the Handsontable instance with new settings.
|
|
1247
1470
|
* @param newSettings The new settings to apply to the Handsontable instance.
|
|
@@ -1258,7 +1481,7 @@ class HotTableComponent {
|
|
|
1258
1481
|
}
|
|
1259
1482
|
}
|
|
1260
1483
|
this.ngZone.runOutsideAngular(() => {
|
|
1261
|
-
this.hotInstance
|
|
1484
|
+
this.hotInstance?.updateSettings(filteredSettings, false);
|
|
1262
1485
|
});
|
|
1263
1486
|
}
|
|
1264
1487
|
/**
|
|
@@ -1290,28 +1513,28 @@ class HotTableComponent {
|
|
|
1290
1513
|
}
|
|
1291
1514
|
return negotiatedSettings;
|
|
1292
1515
|
}
|
|
1293
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
1294
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.
|
|
1516
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotTableComponent, deps: [{ token: HotSettingsResolver }, { token: HotGlobalConfigService }, { token: i0.NgZone }, { token: i0.EnvironmentInjector }, { token: DynamicComponentService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1517
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HotTableComponent, isStandalone: true, selector: "hot-table", inputs: { data: "data", settings: "settings" }, providers: [HotSettingsResolver], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true }], usesOnChanges: true, ngImport: i0, template: '<div #container></div>', isInline: true, styles: [":host{display:block}\n"], encapsulation: i0.ViewEncapsulation.None });
|
|
1295
1518
|
}
|
|
1296
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
1519
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotTableComponent, decorators: [{
|
|
1297
1520
|
type: Component,
|
|
1298
|
-
args: [{ selector: 'hot-table', template: '<div #container></div>',
|
|
1299
|
-
}], ctorParameters: () => [{ type: HotSettingsResolver }, { type: HotGlobalConfigService }, { type: i0.NgZone }, { type: i0.EnvironmentInjector }, { type:
|
|
1521
|
+
args: [{ selector: 'hot-table', template: '<div #container></div>', encapsulation: ViewEncapsulation.None, providers: [HotSettingsResolver], styles: [":host{display:block}\n"] }]
|
|
1522
|
+
}], ctorParameters: () => [{ type: HotSettingsResolver }, { type: HotGlobalConfigService }, { type: i0.NgZone }, { type: i0.EnvironmentInjector }, { type: DynamicComponentService }], propDecorators: { data: [{
|
|
1300
1523
|
type: Input
|
|
1301
1524
|
}], settings: [{
|
|
1302
1525
|
type: Input
|
|
1303
1526
|
}], container: [{
|
|
1304
1527
|
type: ViewChild,
|
|
1305
|
-
args: ['container']
|
|
1528
|
+
args: ['container', { static: false }]
|
|
1306
1529
|
}] } });
|
|
1307
1530
|
|
|
1308
1531
|
class HotTableModule {
|
|
1309
|
-
static version = '
|
|
1310
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.
|
|
1311
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.
|
|
1312
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.
|
|
1532
|
+
static version = '18.0.0-rc1';
|
|
1533
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotTableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
1534
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.25", ngImport: i0, type: HotTableModule, imports: [HotTableComponent], exports: [HotTableComponent] });
|
|
1535
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotTableModule });
|
|
1313
1536
|
}
|
|
1314
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.
|
|
1537
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotTableModule, decorators: [{
|
|
1315
1538
|
type: NgModule,
|
|
1316
1539
|
args: [{
|
|
1317
1540
|
imports: [HotTableComponent],
|