@handsontable/angular-wrapper 0.0.0-next-7c3a081-20260608 → 0.0.0-next-c78aa8c-20260608

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,7 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { ViewContainerRef, ViewChild, Input, ChangeDetectionStrategy, Component, createComponent, EventEmitter, Output, HostBinding, Directive, Injectable, InjectionToken, Inject, ViewEncapsulation, NgModule } from '@angular/core';
2
+ import { ViewContainerRef, ViewChild, Input, ChangeDetectionStrategy, Component, createComponent, Directive, EventEmitter, Output, HostBinding, Injectable, InjectionToken, Inject, inject, DestroyRef, ViewEncapsulation, NgModule } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3
4
  import Handsontable from 'handsontable/base';
4
- import { take } from 'rxjs/operators';
5
+ import { take, skip } from 'rxjs/operators';
5
6
  import { editorFactory } from 'handsontable/editors/factory';
6
7
  import { baseRenderer, rendererFactory, registerRenderer } from 'handsontable/renderers';
7
8
  import { BehaviorSubject } from 'rxjs';
@@ -135,14 +136,7 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
135
136
  this._isPlaceholderReady = true;
136
137
  }
137
138
  this._componentRef = columnMeta._editorComponentReference;
138
- if (this._finishEditSubscription) {
139
- this._finishEditSubscription.unsubscribe();
140
- this._finishEditSubscription = undefined;
141
- }
142
- if (this._cancelEditSubscription) {
143
- this._cancelEditSubscription.unsubscribe();
144
- this._cancelEditSubscription = undefined;
145
- }
139
+ this.cleanupSubscriptions();
146
140
  this._finishEditSubscription = this._componentRef.instance.finishEdit
147
141
  .pipe(take(1))
148
142
  .subscribe(() => {
@@ -161,23 +155,23 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
161
155
  close() {
162
156
  if (this.isOpened()) {
163
157
  this.resetEditorState();
164
- this._editorPlaceHolderRef.changeDetectorRef.detectChanges();
165
- this._editorPlaceHolderRef.instance.detachEditor();
166
- this._componentRef.instance.onClose();
158
+ this._editorPlaceHolderRef?.changeDetectorRef.detectChanges();
159
+ this._editorPlaceHolderRef?.instance.detachEditor();
160
+ this._componentRef?.instance.onClose();
167
161
  }
168
162
  }
169
163
  /**
170
164
  * Focuses the editor. This event is triggered by Handsontable.
171
165
  */
172
166
  focus() {
173
- this._componentRef.instance.onFocus();
167
+ this._componentRef?.instance.onFocus();
174
168
  }
175
169
  /**
176
170
  * Gets the value from the editor.
177
171
  * @returns The value from the editor.
178
172
  */
179
173
  getValue() {
180
- return this._componentRef.instance?.getValue();
174
+ return this._componentRef?.instance?.getValue();
181
175
  }
182
176
  /**
183
177
  * Opens the editor. This event is triggered by Handsontable.
@@ -189,20 +183,23 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
189
183
  open(event) {
190
184
  this.hot.getShortcutManager().setActiveContextName('editor');
191
185
  this.applyPropsToEditor();
192
- this._componentRef.instance.onOpen(event);
186
+ this._componentRef?.instance.onOpen(event);
193
187
  }
194
188
  /**
195
189
  * Sets the value for the custom editor.
196
190
  * @param newValue The value to set.
197
191
  */
198
192
  setValue(newValue) {
199
- this._componentRef.instance?.setValue(newValue);
200
- this._componentRef.changeDetectorRef.detectChanges();
193
+ this._componentRef?.instance?.setValue(newValue);
194
+ this._componentRef?.changeDetectorRef.detectChanges();
201
195
  }
202
196
  /**
203
197
  * Applies properties to the custom editor and editor placeholder.
204
198
  */
205
199
  applyPropsToEditor() {
200
+ if (!this._componentRef || !this._editorPlaceHolderRef) {
201
+ return;
202
+ }
206
203
  const rect = this.getEditedCellRect();
207
204
  if (!this.isInFullEditMode()) {
208
205
  this._componentRef.instance.setValue(null);
@@ -252,14 +249,33 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
252
249
  * Handles the after destroy event.
253
250
  */
254
251
  onAfterDestroy() {
252
+ this.cleanupSubscriptions();
255
253
  this._editorPlaceHolderRef?.destroy();
256
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
+ }
257
270
  /**
258
271
  * Resets the editor placeholder state.
259
272
  * We need to reset the editor placeholder state because we use it
260
273
  * to store multiple references to the custom editor.
261
274
  */
262
275
  resetEditorState() {
276
+ if (!this._editorPlaceHolderRef) {
277
+ return;
278
+ }
263
279
  this._editorPlaceHolderRef.setInput('top', undefined);
264
280
  this._editorPlaceHolderRef.setInput('left', undefined);
265
281
  this._editorPlaceHolderRef.setInput('height', undefined);
@@ -270,44 +286,28 @@ class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
270
286
  }
271
287
 
272
288
  /**
273
- * Abstract base component for creating custom cell renderer components for Handsontable.
289
+ * Shared base directive for HotCellRendererComponent and HotCellRendererAdvancedComponent.
290
+ * Holds all @Input() properties and getProps() that both renderer variants share.
274
291
  *
275
- * This class provides a common interface and properties required by any custom cell renderer.
276
- *
277
- * @template TValue - The type of the component renderer.
292
+ * @template TValue - The type of the rendered cell value.
278
293
  * @template TProps - The type of additional renderer properties.
279
294
  */
280
- class HotCellRendererComponent {
281
- static RENDERER_MARKER = Symbol('HotCellRendererComponent');
295
+ class HotCellRendererBase {
282
296
  value = '';
283
297
  instance;
284
298
  td;
285
299
  row;
286
300
  col;
287
301
  prop;
288
- /**
289
- * The cell properties provided by Handsontable, extended with optional renderer-specific properties.
290
- */
291
302
  cellProperties;
292
- /**
293
- * Retrieves the renderer-specific properties from the cell properties.
294
- *
295
- * @returns The additional properties for the renderer.
296
- */
297
303
  getProps() {
298
304
  return this.cellProperties?.rendererProps ?? {};
299
305
  }
300
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
301
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.24", type: HotCellRendererComponent, isStandalone: true, selector: "hot-cell-renderer", 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 });
306
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
307
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.24", type: HotCellRendererBase, isStandalone: true, inputs: { value: "value", instance: "instance", td: "td", row: "row", col: "col", prop: "prop", cellProperties: "cellProperties" }, ngImport: i0 });
302
308
  }
303
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererComponent, decorators: [{
304
- type: Component,
305
- args: [{
306
- selector: 'hot-cell-renderer',
307
- template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,
308
- standalone: true,
309
- changeDetection: ChangeDetectionStrategy.OnPush,
310
- }]
309
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererBase, decorators: [{
310
+ type: Directive
311
311
  }], propDecorators: { value: [{
312
312
  type: Input
313
313
  }], instance: [{
@@ -325,77 +325,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImpo
325
325
  }] } });
326
326
 
327
327
  /**
328
- * Abstract class representing a Handsontable editor in angular.
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.
329
336
  */
330
- class HotCellEditorComponent {
331
- static EDITOR_MARKER = Symbol('HotCellEditorComponent');
332
- /** The tabindex attribute for the editor. */
333
- tabindex = -1;
334
- /** The data-hot-input attribute for the editor. */
335
- dataHotInput = '';
336
- /** The handsontableInput class for the editor. */
337
- handsontableInputClass = true;
338
- /** The height of the editor as a percentage of the parent container. */
337
+ class HotCellRendererComponent extends HotCellRendererBase {
338
+ static RENDERER_MARKER = Symbol('HotCellRendererComponent');
339
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
340
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.24", 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.24", 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 {
339
359
  heightFitParentContainer = 100;
340
- /** The width of the editor as a percentage of the parent container. */
341
360
  widthFitParentContainer = 100;
342
- /** The row index of the cell being edited. */
343
361
  row;
344
- /** The column index of the cell being edited. */
345
362
  column;
346
- /** The property name of the cell being edited. */
347
363
  prop;
348
- /** The original value of the cell being edited. */
349
364
  originalValue;
350
- /** The cell properties of the cell being edited. */
351
365
  cellProperties;
352
- /** Event emitted when the edit is finished.
353
- * The data will be saved to the model.
354
- */
355
366
  finishEdit = new EventEmitter();
356
- /** Event emitted when the edit is canceled.
357
- * The entered data will be reverted to the original value.
358
- */
359
367
  cancelEdit = new EventEmitter();
360
- /** The current value of the editor. */
361
- _value;
362
- /** Event triggered by Handsontable on closing the editor.
363
- * The user can define their own actions for
364
- * the custom editor to be called after the base logic. */
365
- onClose() { }
366
- /** Event triggered by Handsontable on open the editor.
367
- * The user can define their own actions for
368
- * the custom editor to be called after the base logic. */
369
- onOpen(event) { }
370
- /**
371
- * Gets the current value of the editor.
372
- * @returns The current value of the editor.
373
- */
368
+ value;
374
369
  getValue() {
375
- return this._value;
370
+ return this.value;
376
371
  }
377
- /**
378
- * Sets the current value of the editor.
379
- * @param value The value to set.
380
- */
381
372
  setValue(value) {
382
- this._value = value;
373
+ this.value = value;
383
374
  }
384
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
385
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.24", type: HotCellEditorComponent, isStandalone: true, inputs: { row: "row", column: "column", prop: "prop", originalValue: "originalValue", cellProperties: "cellProperties" }, outputs: { finishEdit: "finishEdit", cancelEdit: "cancelEdit" }, host: { properties: { "attr.tabindex": "this.tabindex", "attr.data-hot-input": "this.dataHotInput", "class.handsontableInput": "this.handsontableInputClass", "style.height.%": "this.heightFitParentContainer", "style.width.%": "this.widthFitParentContainer" } }, ngImport: i0 });
375
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
376
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.24", 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 });
386
377
  }
387
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorComponent, decorators: [{
378
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorBase, decorators: [{
388
379
  type: Directive
389
- }], propDecorators: { tabindex: [{
390
- type: HostBinding,
391
- args: ['attr.tabindex']
392
- }], dataHotInput: [{
393
- type: HostBinding,
394
- args: ['attr.data-hot-input']
395
- }], handsontableInputClass: [{
396
- type: HostBinding,
397
- args: ['class.handsontableInput']
398
- }], heightFitParentContainer: [{
380
+ }], propDecorators: { heightFitParentContainer: [{
399
381
  type: HostBinding,
400
382
  args: ['style.height.%']
401
383
  }], widthFitParentContainer: [{
@@ -417,6 +399,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImpo
417
399
  type: Output
418
400
  }] } });
419
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.24", ngImport: i0, type: HotCellEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
422
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.24", 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.24", 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
+
420
437
  /**
421
438
  * Factory function to create a custom Handsontable editor adapter for Angular components.
422
439
  *
@@ -437,7 +454,7 @@ const FactoryEditorAdapter = (componentRef) => editorFactory({
437
454
  editor._finishEditSubscription = undefined;
438
455
  editor._cancelEditSubscription = undefined;
439
456
  createEditorPlaceholder(editor, editor.hot._angularEnvironmentInjector);
440
- editor.input = editor._editorPlaceHolderRef.location.nativeElement;
457
+ editor.input = editor._editorPlaceHolderRef?.location.nativeElement ?? document.createElement('div');
441
458
  editor._afterRowResizeCallback = () => {
442
459
  if (editor.isOpened()) {
443
460
  applyPropsToEditor(editor);
@@ -449,6 +466,7 @@ const FactoryEditorAdapter = (componentRef) => editorFactory({
449
466
  }
450
467
  };
451
468
  editor._afterDestroyCallback = () => {
469
+ cleanupSubscriptions(editor);
452
470
  if (editor._editorPlaceHolderRef) {
453
471
  editor._editorPlaceHolderRef.destroy();
454
472
  }
@@ -476,8 +494,8 @@ const FactoryEditorAdapter = (componentRef) => editorFactory({
476
494
  onFocus: (editor) => editor._componentRef.instance.onFocus?.(editor),
477
495
  afterClose: (editor) => {
478
496
  resetEditorState(editor);
479
- editor._editorPlaceHolderRef.changeDetectorRef.detectChanges();
480
- editor._editorPlaceHolderRef.instance.detachEditor();
497
+ editor._editorPlaceHolderRef?.changeDetectorRef.detectChanges();
498
+ editor._editorPlaceHolderRef?.instance.detachEditor();
481
499
  editor._componentRef.instance.afterClose?.(editor);
482
500
  },
483
501
  getValue: (editor) => editor._componentRef.instance.getValue(),
@@ -556,33 +574,16 @@ function cleanupSubscriptions(editor) {
556
574
  /**
557
575
  * Abstract base component for creating advanced custom cell renderer components for Handsontable.
558
576
  *
559
- * This class provides a common interface and properties required by any custom cell renderer.
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.
560
579
  *
561
580
  * @template TValue - The type of the component renderer.
562
581
  * @template TProps - The type of additional renderer properties.
563
582
  */
564
- class HotCellRendererAdvancedComponent {
583
+ class HotCellRendererAdvancedComponent extends HotCellRendererBase {
565
584
  static RENDERER_MARKER = Symbol('HotCellRendererAdvancedComponent');
566
- value = '';
567
- instance;
568
- td;
569
- row;
570
- col;
571
- prop;
572
- /**
573
- * The cell properties provided by Handsontable, extended with optional renderer-specific properties.
574
- */
575
- cellProperties;
576
- /**
577
- * Retrieves the renderer-specific properties from the cell properties.
578
- *
579
- * @returns The additional properties for the renderer.
580
- */
581
- getProps() {
582
- return this.cellProperties?.rendererProps ?? {};
583
- }
584
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererAdvancedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
585
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.24", 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.24", ngImport: i0, type: HotCellRendererAdvancedComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
586
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.24", 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 });
586
587
  }
587
588
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellRendererAdvancedComponent, decorators: [{
588
589
  type: Component,
@@ -592,111 +593,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImpo
592
593
  standalone: true,
593
594
  changeDetection: ChangeDetectionStrategy.OnPush,
594
595
  }]
595
- }], propDecorators: { value: [{
596
- type: Input
597
- }], instance: [{
598
- type: Input
599
- }], td: [{
600
- type: Input
601
- }], row: [{
602
- type: Input
603
- }], col: [{
604
- type: Input
605
- }], prop: [{
606
- type: Input
607
- }], cellProperties: [{
608
- type: Input
609
- }] } });
596
+ }] });
610
597
 
611
598
  /**
612
- * Abstract class representing a Handsontable editor in angular.
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.
613
604
  */
614
- class HotCellEditorAdvancedComponent {
605
+ class HotCellEditorAdvancedComponent extends HotCellEditorBase {
615
606
  static EDITOR_MARKER = Symbol('HotCellEditorAdvancedComponent');
616
- /** The height of the editor as a percentage of the parent container. */
617
- heightFitParentContainer = 100;
618
- /** The width of the editor as a percentage of the parent container. */
619
- widthFitParentContainer = 100;
620
- /** The row index of the cell being edited. */
621
- row;
622
- /** The column index of the cell being edited. */
623
- column;
624
- /** The property name of the cell being edited. */
625
- prop;
626
- /** The original value of the cell being edited. */
627
- originalValue;
628
- /** The cell properties of the cell being edited. */
629
- cellProperties;
630
- /** Event emitted when the edit is finished.
631
- * The data will be saved to the model.
632
- */
633
- finishEdit = new EventEmitter();
634
- /** Event emitted when the edit is canceled.
635
- * The entered data will be reverted to the original value.
636
- */
637
- cancelEdit = new EventEmitter();
638
- /** The current value of the editor. */
639
- value;
640
- /** Event triggered by Handsontable on focus the editor.
641
- * The user have to define focus logic.
642
- */
607
+ /** Event triggered by Handsontable on focusing the editor. Available in advanced mode. */
643
608
  onFocus(editor) { }
644
- /**
645
- * Gets the current value of the editor.
646
- * @returns The current value of the editor.
647
- */
648
- getValue() {
649
- return this.value;
650
- }
651
- /**
652
- * Sets the current value of the editor.
653
- * @param value The value to set.
654
- */
655
- setValue(value) {
656
- this.value = value;
657
- }
658
- /** 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. */
659
610
  position = 'container';
660
611
  /** The shortcuts available for the editor. Available in advanced mode. */
661
612
  shortcuts;
662
- /** The group name for the shortcuts. Available in advanced mode.*/
613
+ /** The group name for the shortcuts. Available in advanced mode. */
663
614
  shortcutsGroup;
664
- /** Configuration. Available in advanced mode. */
615
+ /** Configuration object. Available in advanced mode. */
665
616
  config;
666
- /** 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. */
667
618
  afterOpen(editor, event) { }
668
619
  /** Lifecycle hook called after the editor is closed. Available in advanced mode. */
669
620
  afterClose(editor) { }
670
- /** 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. */
671
622
  afterInit(editor) { }
672
623
  /** Lifecycle hook called before the editor is opened. Available in advanced mode. */
673
624
  beforeOpen(editor, { row, col, prop, td, originalValue, cellProperties, }) { }
674
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorAdvancedComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
675
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.24", type: HotCellEditorAdvancedComponent, 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 });
625
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorAdvancedComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
626
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.24", type: HotCellEditorAdvancedComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
676
627
  }
677
628
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotCellEditorAdvancedComponent, decorators: [{
678
629
  type: Directive
679
- }], propDecorators: { heightFitParentContainer: [{
680
- type: HostBinding,
681
- args: ['style.height.%']
682
- }], widthFitParentContainer: [{
683
- type: HostBinding,
684
- args: ['style.width.%']
685
- }], row: [{
686
- type: Input
687
- }], column: [{
688
- type: Input
689
- }], prop: [{
690
- type: Input
691
- }], originalValue: [{
692
- type: Input
693
- }], cellProperties: [{
694
- type: Input
695
- }], finishEdit: [{
696
- type: Output
697
- }], cancelEdit: [{
698
- type: Output
699
- }] } });
630
+ }] });
700
631
 
701
632
  const INVALID_RENDERER_WARNING = 'The provided renderer component was not recognized as a valid custom renderer. ' +
702
633
  'It must either extend HotCellRendererComponent or be a valid TemplateRef. ' +
@@ -704,6 +635,11 @@ const INVALID_RENDERER_WARNING = 'The provided renderer component was not recogn
704
635
  const INVALID_ADVANCED_RENDERER_WARNING = 'The provided renderer component was not recognized as a valid custom renderer. ' +
705
636
  'It must either extend HotCellRendererAdvancedComponent. ' +
706
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
+ ];
707
643
  /**
708
644
  * Type guard that checks if the given object is a TemplateRef.
709
645
  *
@@ -711,7 +647,7 @@ const INVALID_ADVANCED_RENDERER_WARNING = 'The provided renderer component was n
711
647
  * @returns True if the object is a TemplateRef; otherwise, false.
712
648
  */
713
649
  function isTemplateRef(obj) {
714
- return obj && typeof obj.createEmbeddedView === 'function';
650
+ return !!obj && typeof obj.createEmbeddedView === 'function';
715
651
  }
716
652
  /**
717
653
  * Type guard to check if an object is an instance of HotCellRendererComponent.
@@ -744,6 +680,23 @@ function isAdvancedHotCellRendererComponent(obj) {
744
680
  class DynamicComponentService {
745
681
  appRef;
746
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();
747
700
  constructor(appRef, environmentInjector) {
748
701
  this.appRef = appRef;
749
702
  this.environmentInjector = environmentInjector;
@@ -758,6 +711,7 @@ class DynamicComponentService {
758
711
  * @returns A renderer function that can be used in Handsontable's configuration.
759
712
  */
760
713
  createRendererFromComponent(component, componentProps = {}, register = false) {
714
+ let registered = false;
761
715
  return (instance, td, row, col, prop, value, cellProperties) => {
762
716
  const properties = {
763
717
  value,
@@ -768,24 +722,24 @@ class DynamicComponentService {
768
722
  prop,
769
723
  cellProperties,
770
724
  };
771
- if (componentProps) {
772
- Object.assign(cellProperties, { rendererProps: componentProps });
773
- }
774
- const rendererParameters = [instance, td, row, col, prop, value, cellProperties];
775
- baseRenderer.apply(this, rendererParameters);
776
- td.innerHTML = '';
725
+ cellProperties.rendererProps = componentProps;
726
+ baseRenderer.call(this, instance, td, row, col, prop, value, cellProperties);
727
+ this.registerSweepHook(instance);
777
728
  if (isTemplateRef(component)) {
778
- this.attachTemplateToElement(component, td, properties);
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);
779
733
  }
780
734
  else if (isHotCellRendererComponent(component)) {
781
- const componentRef = this.createComponent(component, properties);
782
- this.attachComponentToElement(componentRef, td);
735
+ this.renderComponent(td, component, properties);
783
736
  }
784
737
  else {
785
738
  console.warn(INVALID_RENDERER_WARNING);
786
739
  }
787
- if (register && isHotCellRendererComponent(component)) {
788
- Handsontable.renderers.registerRenderer(component.constructor.name, component);
740
+ if (register && !registered && isHotCellRendererComponent(component)) {
741
+ Handsontable.renderers.registerRenderer(component.name, component);
742
+ registered = true;
789
743
  }
790
744
  return td;
791
745
  };
@@ -800,6 +754,7 @@ class DynamicComponentService {
800
754
  * @returns A renderer function that can be used in Handsontable's configuration.
801
755
  */
802
756
  createRendererWithFactory(component, componentProps = {}, register = false) {
757
+ let registered = false;
803
758
  return rendererFactory(({ instance, td, row, column, prop, value, cellProperties }) => {
804
759
  const properties = {
805
760
  value,
@@ -810,28 +765,161 @@ class DynamicComponentService {
810
765
  prop,
811
766
  cellProperties,
812
767
  };
813
- if (componentProps) {
814
- Object.assign(cellProperties, { rendererProps: componentProps });
815
- }
816
- td.innerHTML = '';
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);
817
773
  if (isAdvancedHotCellRendererComponent(component)) {
818
- const componentRef = this.createComponent(component, properties);
819
- this.attachComponentToElement(componentRef, td);
774
+ this.renderComponent(td, component, properties);
820
775
  }
821
776
  else {
822
777
  console.warn(INVALID_ADVANCED_RENDERER_WARNING);
823
778
  }
824
- if (register && isAdvancedHotCellRendererComponent(component)) {
825
- registerRenderer(component.constructor.name, 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);
826
858
  }
827
859
  });
828
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
+ }
829
916
  /**
830
917
  * Attaches an embedded view created from a TemplateRef to a given DOM element.
831
918
  *
832
919
  * @param template - The TemplateRef to create an embedded view from.
833
920
  * @param tdEl - The target DOM element (a table cell) to which the view will be appended.
834
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.
835
923
  */
836
924
  attachTemplateToElement(template, tdEl, properties) {
837
925
  const embeddedView = template.createEmbeddedView({
@@ -842,6 +930,7 @@ class DynamicComponentService {
842
930
  embeddedView.rootNodes.forEach((node) => {
843
931
  tdEl.appendChild(node);
844
932
  });
933
+ return embeddedView;
845
934
  }
846
935
  /**
847
936
  * Dynamically creates an Angular component of the given type.
@@ -854,18 +943,46 @@ class DynamicComponentService {
854
943
  const componentRef = createComponent(component, {
855
944
  environmentInjector: this.environmentInjector,
856
945
  });
857
- Object.keys(rendererParameters).forEach((key) => {
858
- if (Object.prototype.hasOwnProperty.call(rendererParameters, key)) {
859
- componentRef.setInput(key, rendererParameters[key]);
860
- }
861
- else {
862
- console.warn(`Input property "${key}" does not exist on component instance: ${component?.name}.`);
863
- }
864
- });
946
+ this.applyInputs(componentRef, rendererParameters);
865
947
  componentRef.changeDetectorRef.detectChanges();
866
948
  this.appRef.attachView(componentRef.hostView);
867
949
  return componentRef;
868
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
+ }
869
986
  /**
870
987
  * Attaches a dynamically created component's view to a specified DOM container element.
871
988
  *
@@ -873,18 +990,40 @@ class DynamicComponentService {
873
990
  * @param container - The target DOM element to which the component's root node will be appended.
874
991
  */
875
992
  attachComponentToElement(componentRef, container) {
876
- const domElem = componentRef.hostView.rootNodes[0];
877
- container.appendChild(domElem);
993
+ componentRef.hostView.rootNodes.forEach((node) => {
994
+ container.appendChild(node);
995
+ });
878
996
  }
879
997
  /**
880
998
  * Destroys a dynamically created component and detaches its view from the Angular application.
881
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
+ *
882
1005
  * @param componentRef - The reference to the component to be destroyed.
883
1006
  */
884
1007
  destroyComponent(componentRef) {
885
- this.appRef.detachView(componentRef.hostView);
1008
+ const hostView = componentRef.hostView;
1009
+ if (hostView.destroyed) {
1010
+ return;
1011
+ }
1012
+ this.appRef.detachView(hostView);
886
1013
  componentRef.destroy();
887
1014
  }
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
+ }
888
1027
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: DynamicComponentService, deps: [{ token: i0.ApplicationRef }, { token: i0.EnvironmentInjector }], target: i0.ɵɵFactoryTarget.Injectable });
889
1028
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: DynamicComponentService, providedIn: 'root' });
890
1029
  }
@@ -896,6 +1035,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImpo
896
1035
  }], ctorParameters: () => [{ type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }] });
897
1036
 
898
1037
  const AVAILABLE_HOOKS_SET = new Set(Handsontable.hooks.getRegistered());
1038
+ const HOT_ZONE_WRAPPED = Symbol('hotZoneWrapped');
899
1039
  /**
900
1040
  * Service to resolve and apply custom settings for Handsontable settings object.
901
1041
  */
@@ -911,15 +1051,26 @@ class HotSettingsResolver {
911
1051
  /**
912
1052
  * Applies custom settings to the provided GridSettings.
913
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.
914
1057
  * @returns The merged grid settings with custom settings applied.
915
1058
  */
916
- applyCustomSettings(settings) {
917
- const mergedSettings = settings;
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
+ }
918
1069
  this.updateColumnRendererForGivenCustomRenderer(mergedSettings);
919
- this.updateColumnEditorForGivenCustomEditor(mergedSettings);
1070
+ this.updateColumnEditorForGivenCustomEditor(mergedSettings, previousColumns);
920
1071
  this.updateColumnValidatorForGivenCustomValidator(mergedSettings);
921
1072
  this.wrapHooksInNgZone(mergedSettings);
922
- return mergedSettings ?? {};
1073
+ return mergedSettings;
923
1074
  }
924
1075
  /**
925
1076
  * Ensures that hook callbacks in the provided grid settings run inside Angular's zone.
@@ -928,12 +1079,18 @@ class HotSettingsResolver {
928
1079
  */
929
1080
  wrapHooksInNgZone(settings) {
930
1081
  const ngZone = this.ngZone;
931
- AVAILABLE_HOOKS_SET.forEach((key) => {
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
+ }
932
1087
  const option = settings[key];
933
- if (typeof option === 'function') {
934
- settings[key] = function (...args) {
1088
+ if (typeof option === 'function' && !option[HOT_ZONE_WRAPPED]) {
1089
+ const wrapped = function (...args) {
935
1090
  return ngZone.run(() => option.apply(this, args));
936
1091
  };
1092
+ wrapped[HOT_ZONE_WRAPPED] = true;
1093
+ settings[key] = wrapped;
937
1094
  }
938
1095
  });
939
1096
  }
@@ -960,31 +1117,73 @@ class HotSettingsResolver {
960
1117
  }
961
1118
  /**
962
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
+ *
963
1124
  * @param mergedSettings The merged grid settings.
1125
+ * @param previousColumns The previously resolved columns, used to recycle editor components.
964
1126
  */
965
- updateColumnEditorForGivenCustomEditor(mergedSettings) {
1127
+ updateColumnEditorForGivenCustomEditor(mergedSettings, previousColumns) {
966
1128
  if (!Array.isArray(mergedSettings?.columns)) {
967
1129
  return;
968
1130
  }
969
- mergedSettings?.columns
970
- ?.filter((settings) => this.isEditorComponentRefType(settings.editor) || this.isAdvancedEditorComponentRefType(settings.editor))
971
- ?.forEach((cellSettings) => {
972
- if (this.isAdvancedEditorComponentRefType(cellSettings.editor)) {
973
- const component = createComponent(cellSettings.editor, {
974
- environmentInjector: this.environmentInjector,
975
- });
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) {
976
1150
  cellSettings.editor = FactoryEditorAdapter(component);
977
1151
  }
978
1152
  else {
979
- const component = createComponent(cellSettings.editor, {
980
- environmentInjector: this.environmentInjector,
981
- });
982
- cellSettings['_editorComponentReference'] = component;
983
- cellSettings['_environmentInjector'] = this.environmentInjector;
1153
+ internalSettings._environmentInjector = this.environmentInjector;
984
1154
  cellSettings.editor = BaseEditorAdapter;
985
1155
  }
986
1156
  });
987
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
+ }
988
1187
  /**
989
1188
  * Updates the column validator for columns with a custom validator.
990
1189
  * @param mergedSettings The merged grid settings.
@@ -1152,6 +1351,7 @@ class HotTableComponent {
1152
1351
  _hotConfig;
1153
1352
  ngZone;
1154
1353
  environmentInjector;
1354
+ _dynamicComponentService;
1155
1355
  // component inputs
1156
1356
  /** The data for the Handsontable instance. */
1157
1357
  data = null;
@@ -1161,19 +1361,20 @@ class HotTableComponent {
1161
1361
  container;
1162
1362
  /** The Handsontable instance. */
1163
1363
  __hotInstance = null;
1164
- configSubscription;
1165
- constructor(_hotSettingsResolver, _hotConfig, ngZone, environmentInjector) {
1364
+ _destroyRef = inject(DestroyRef);
1365
+ constructor(_hotSettingsResolver, _hotConfig, ngZone, environmentInjector, _dynamicComponentService) {
1166
1366
  this._hotSettingsResolver = _hotSettingsResolver;
1167
1367
  this._hotConfig = _hotConfig;
1168
1368
  this.ngZone = ngZone;
1169
1369
  this.environmentInjector = environmentInjector;
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 || (this.__hotInstance && !this.__hotInstance.isDestroyed)) {
1377
+ if (!this.__hotInstance || !this.__hotInstance.isDestroyed) {
1177
1378
  // Will return the Handsontable instance or `null` if it's not yet been created.
1178
1379
  return this.__hotInstance;
1179
1380
  }
@@ -1202,7 +1403,7 @@ class HotTableComponent {
1202
1403
  this.hotInstance._angularEnvironmentInjector = this.environmentInjector;
1203
1404
  this.hotInstance.init();
1204
1405
  });
1205
- this.configSubscription = this._hotConfig.config$.subscribe((config) => {
1406
+ this._hotConfig.config$.pipe(skip(1), takeUntilDestroyed(this._destroyRef)).subscribe((config) => {
1206
1407
  if (this.hotInstance) {
1207
1408
  const negotiatedSettings = this.getNegotiatedSettings(this.settings);
1208
1409
  this.updateHotTable(negotiatedSettings);
@@ -1214,8 +1415,28 @@ class HotTableComponent {
1214
1415
  return;
1215
1416
  }
1216
1417
  if (changes.settings && !changes.settings.firstChange) {
1217
- const newOptions = this._hotSettingsResolver.applyCustomSettings(changes.settings.currentValue);
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.
1218
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
1442
  this.hotInstance?.updateData(changes.data.currentValue);
@@ -1226,10 +1447,14 @@ class HotTableComponent {
1226
1447
  */
1227
1448
  ngOnDestroy() {
1228
1449
  this.ngZone.runOutsideAngular(() => {
1229
- if (!this.hotInstance) {
1450
+ if (!this.__hotInstance || this.__hotInstance.isDestroyed) {
1230
1451
  return;
1231
1452
  }
1232
- const columns = this.hotInstance.getSettings().columns;
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;
1233
1458
  if (columns && Array.isArray(columns)) {
1234
1459
  columns.forEach((column) => {
1235
1460
  if (column._editorComponentReference) {
@@ -1237,9 +1462,8 @@ class HotTableComponent {
1237
1462
  }
1238
1463
  });
1239
1464
  }
1240
- this.hotInstance.destroy();
1465
+ this.__hotInstance.destroy();
1241
1466
  });
1242
- this.configSubscription.unsubscribe();
1243
1467
  }
1244
1468
  /**
1245
1469
  * Updates the Handsontable instance with new settings.
@@ -1289,13 +1513,13 @@ class HotTableComponent {
1289
1513
  }
1290
1514
  return negotiatedSettings;
1291
1515
  }
1292
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotTableComponent, deps: [{ token: HotSettingsResolver }, { token: HotGlobalConfigService }, { token: i0.NgZone }, { token: i0.EnvironmentInjector }], target: i0.ɵɵFactoryTarget.Component });
1516
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotTableComponent, deps: [{ token: HotSettingsResolver }, { token: HotGlobalConfigService }, { token: i0.NgZone }, { token: i0.EnvironmentInjector }, { token: DynamicComponentService }], target: i0.ɵɵFactoryTarget.Component });
1293
1517
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.24", 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 });
1294
1518
  }
1295
1519
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.24", ngImport: i0, type: HotTableComponent, decorators: [{
1296
1520
  type: Component,
1297
1521
  args: [{ selector: 'hot-table', template: '<div #container></div>', encapsulation: ViewEncapsulation.None, providers: [HotSettingsResolver], styles: [":host{display:block}\n"] }]
1298
- }], ctorParameters: () => [{ type: HotSettingsResolver }, { type: HotGlobalConfigService }, { type: i0.NgZone }, { type: i0.EnvironmentInjector }], propDecorators: { data: [{
1522
+ }], ctorParameters: () => [{ type: HotSettingsResolver }, { type: HotGlobalConfigService }, { type: i0.NgZone }, { type: i0.EnvironmentInjector }, { type: DynamicComponentService }], propDecorators: { data: [{
1299
1523
  type: Input
1300
1524
  }], settings: [{
1301
1525
  type: Input