@handsontable/angular-wrapper
Version:
Best Data Grid for Angular with Spreadsheet Look and Feel.
1,088 lines (1,077 loc) • 73.3 kB
JavaScript
import * as i0 from '@angular/core';
import { ViewContainerRef, ViewChild, Input, ChangeDetectionStrategy, Component, createComponent, Directive, EventEmitter, Output, HostBinding, Injectable, InjectionToken, Inject, inject, DestroyRef, ViewEncapsulation, NgModule } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import Handsontable from 'handsontable/base';
import { take, skip } from 'rxjs/operators';
import { editorFactory } from 'handsontable/editors/factory';
import { baseRenderer, rendererFactory, registerRenderer } from 'handsontable/renderers';
import { BehaviorSubject } from 'rxjs';
/**
* Component representing a placeholder for a custom editor in Handsontable.
* It is used only within the wrapper.
*/
class CustomEditorPlaceholderComponent {
/** The top position of the editor. */
top;
/** The left position of the editor. */
left;
/** The height of the editor. */
height;
/** The width of the editor. */
width;
set isVisible(value) {
this._isVisible = value;
}
placeholderCustomClass = 'handsontableInputHolder ht_clone_master';
/** The reference to the component instance of the editor. */
set componentRef(hotEditorComponentRef) {
if (hotEditorComponentRef) {
this.container.insert(hotEditorComponentRef.hostView);
}
}
/** The container for the editor's input placeholder. */
container;
/** The display style of the editor. */
get display() {
return this._isVisible ? 'block' : 'none';
}
_isVisible = false;
/**
* Detaches the container from the Handsontable.
*/
detachEditor() {
this.container.detach();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CustomEditorPlaceholderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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
[class]="placeholderCustomClass"
[style.display]="display"
[style.width.px]="width"
[style.height.px]="height"
[style.maxWidth.px]="width"
[style.maxHeight.px]="height"
[style.top.px]="top"
[style.left.px]="left"
>
<ng-template #inputPlaceholder></ng-template>
</div>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CustomEditorPlaceholderComponent, decorators: [{
type: Component,
args: [{
template: ` <div
[class]="placeholderCustomClass"
[style.display]="display"
[style.width.px]="width"
[style.height.px]="height"
[style.maxWidth.px]="width"
[style.maxHeight.px]="height"
[style.top.px]="top"
[style.left.px]="left"
>
<ng-template #inputPlaceholder></ng-template>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
}]
}], propDecorators: { top: [{
type: Input
}], left: [{
type: Input
}], height: [{
type: Input
}], width: [{
type: Input
}], isVisible: [{
type: Input
}], placeholderCustomClass: [{
type: Input
}], componentRef: [{
type: Input
}], container: [{
type: ViewChild,
args: ['inputPlaceholder', { read: ViewContainerRef, static: true }]
}] } });
/**
* Adapter for BaseEditor from Handsontable.
*/
class BaseEditorAdapter extends Handsontable.editors.BaseEditor {
/** Reference to the custom editor component. */
_componentRef;
/** Reference to the editor placeholder component. */
_editorPlaceHolderRef;
/** Flag indicating whether the placeholder is ready. */
_isPlaceholderReady = false;
/** Subscription for the finish edit event. */
_finishEditSubscription;
/** Subscription for the cancel edit event. */
_cancelEditSubscription;
/**
* Creates an instance of BaseEditorAdapter.
* @param instance The Handsontable instance.
*/
constructor(instance) {
super(instance);
this.hot.addHook('afterRowResize', this.onAfterRowResize.bind(this));
this.hot.addHook('afterColumnResize', this.onAfterColumnResize.bind(this));
this.hot.addHook('afterDestroy', this.onAfterDestroy.bind(this));
}
/**
* Prepares the editor for editing. Parameters are passed from Handsontable.
* @param row The row index.
* @param column The column index.
* @param prop The property name.
* @param TD The table cell element.
* @param originalValue The original value of the cell.
* @param cellProperties The cell properties.
*/
prepare(row, column, prop, TD, originalValue, cellProperties) {
if (!this.isOpened()) {
super.prepare(row, column, prop, TD, originalValue, cellProperties);
const columnMeta = this.hot.getColumnMeta(column);
if (!this._isPlaceholderReady) {
this.createEditorPlaceholder(columnMeta._environmentInjector);
this._isPlaceholderReady = true;
}
this._componentRef = columnMeta._editorComponentReference;
this.cleanupSubscriptions();
this._finishEditSubscription = this._componentRef.instance.finishEdit
.pipe(take(1))
.subscribe(() => {
this.finishEditing();
});
this._cancelEditSubscription = this._componentRef.instance.cancelEdit
.pipe(take(1))
.subscribe(() => {
this.cancelChanges();
});
}
}
/**
* Closes the editor. This event is triggered by Handsontable.
*/
close() {
if (this.isOpened()) {
this.resetEditorState();
this._editorPlaceHolderRef?.changeDetectorRef.detectChanges();
this._editorPlaceHolderRef?.instance.detachEditor();
this._componentRef?.instance.onClose();
}
}
/**
* Focuses the editor. This event is triggered by Handsontable.
*/
focus() {
this._componentRef?.instance.onFocus();
}
/**
* Gets the value from the editor.
* @returns The value from the editor.
*/
getValue() {
return this._componentRef?.instance?.getValue();
}
/**
* Opens the editor. This event is triggered by Handsontable.
* When opening, we set the shortcut context to 'editor'.
* This allows the built-in keyboard shortcuts to operate within the editor.
* @param event The event that triggered the opening of the editor.
* @remarks When entering edit mode using double-click, keyboard shortcuts do not work.
*/
open(event) {
this.hot.getShortcutManager().setActiveContextName('editor');
this.applyPropsToEditor();
this._componentRef?.instance.onOpen(event);
}
/**
* Sets the value for the custom editor.
* @param newValue The value to set.
*/
setValue(newValue) {
this._componentRef?.instance?.setValue(newValue);
this._componentRef?.changeDetectorRef.detectChanges();
}
/**
* Applies properties to the custom editor and editor placeholder.
*/
applyPropsToEditor() {
if (!this._componentRef || !this._editorPlaceHolderRef) {
return;
}
const rect = this.getEditedCellRect();
if (!this.isInFullEditMode()) {
this._componentRef.instance.setValue(null);
}
this._componentRef.setInput('originalValue', this.originalValue);
this._componentRef.setInput('row', this.row);
this._componentRef.setInput('column', this.col);
this._componentRef.setInput('prop', this.prop);
this._componentRef.setInput('cellProperties', this.cellProperties);
this._editorPlaceHolderRef.setInput('top', rect.top);
this._editorPlaceHolderRef.setInput('left', rect.start);
this._editorPlaceHolderRef.setInput('height', rect.height);
this._editorPlaceHolderRef.setInput('width', rect.width);
this._editorPlaceHolderRef.setInput('isVisible', true);
this._editorPlaceHolderRef.setInput('componentRef', this._componentRef);
this._editorPlaceHolderRef.changeDetectorRef.detectChanges();
}
/**
* Creates the editor placeholder and append it to hot rootElement.
* @param injector The environment injector.
*/
createEditorPlaceholder(injector) {
this._editorPlaceHolderRef = createComponent(CustomEditorPlaceholderComponent, {
environmentInjector: injector,
});
this.hot.rootElement.appendChild(this._editorPlaceHolderRef.location.nativeElement);
}
/**
* Handles the after column resize event.
* Helps adjust the editor size to the column size and update its position.
*/
onAfterColumnResize() {
if (this.isOpened()) {
this.applyPropsToEditor();
}
}
/**
* Handles the after row resize event.
* Helps adjust the editor size to the column size and update its position.
*/
onAfterRowResize() {
if (this.isOpened()) {
this.applyPropsToEditor();
}
}
/**
* Handles the after destroy event.
*/
onAfterDestroy() {
this.cleanupSubscriptions();
this._editorPlaceHolderRef?.destroy();
}
/**
* Unsubscribes the finish/cancel edit subscriptions if they are still active.
* Without this, destroying the table while an editor was prepared but never
* finished/cancelled leaves the `take(1)` subscriptions hanging (they never emit).
*/
cleanupSubscriptions() {
if (this._finishEditSubscription) {
this._finishEditSubscription.unsubscribe();
this._finishEditSubscription = undefined;
}
if (this._cancelEditSubscription) {
this._cancelEditSubscription.unsubscribe();
this._cancelEditSubscription = undefined;
}
}
/**
* Resets the editor placeholder state.
* We need to reset the editor placeholder state because we use it
* to store multiple references to the custom editor.
*/
resetEditorState() {
if (!this._editorPlaceHolderRef) {
return;
}
this._editorPlaceHolderRef.setInput('top', undefined);
this._editorPlaceHolderRef.setInput('left', undefined);
this._editorPlaceHolderRef.setInput('height', undefined);
this._editorPlaceHolderRef.setInput('width', undefined);
this._editorPlaceHolderRef.setInput('isVisible', false);
this._editorPlaceHolderRef.setInput('componentRef', undefined);
}
}
/**
* Shared base directive for HotCellRendererComponent and HotCellRendererAdvancedComponent.
* Holds all @Input() properties and getProps() that both renderer variants share.
*
* @template TValue - The type of the rendered cell value.
* @template TProps - The type of additional renderer properties.
*/
class HotCellRendererBase {
value = '';
instance;
td;
row;
col;
prop;
cellProperties;
getProps() {
return this.cellProperties?.rendererProps ?? {};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererBase, decorators: [{
type: Directive
}], propDecorators: { value: [{
type: Input
}], instance: [{
type: Input
}], td: [{
type: Input
}], row: [{
type: Input
}], col: [{
type: Input
}], prop: [{
type: Input
}], cellProperties: [{
type: Input
}] } });
/**
* Abstract base component for creating custom cell renderer components for Handsontable.
*
* Extend this component and provide your own template to implement a custom renderer.
* Value type is limited to primitives (`string | number | boolean`).
* For object and array values use {@link HotCellRendererAdvancedComponent}.
*
* @template TValue - The type of the component renderer.
* @template TProps - The type of additional renderer properties.
*/
class HotCellRendererComponent extends HotCellRendererBase {
static RENDERER_MARKER = Symbol('HotCellRendererComponent');
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererComponent, decorators: [{
type: Component,
args: [{
selector: 'hot-cell-renderer',
template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
}]
}] });
/**
* Shared base directive for HotCellEditorComponent and HotCellEditorAdvancedComponent.
* Holds all @Input(), @Output() and @HostBinding() declarations that both editor variants share.
*
* @template T - The type of the edited cell value.
*/
class HotCellEditorBase {
heightFitParentContainer = 100;
widthFitParentContainer = 100;
row;
column;
prop;
originalValue;
cellProperties;
finishEdit = new EventEmitter();
cancelEdit = new EventEmitter();
value;
getValue() {
return this.value;
}
setValue(value) {
this.value = value;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorBase, decorators: [{
type: Directive
}], propDecorators: { heightFitParentContainer: [{
type: HostBinding,
args: ['style.height.%']
}], widthFitParentContainer: [{
type: HostBinding,
args: ['style.width.%']
}], row: [{
type: Input
}], column: [{
type: Input
}], prop: [{
type: Input
}], originalValue: [{
type: Input
}], cellProperties: [{
type: Input
}], finishEdit: [{
type: Output
}], cancelEdit: [{
type: Output
}] } });
/**
* Abstract class representing a basic Handsontable cell editor in Angular.
*
* Extend this class and decorate the subclass with `@Component()` to implement a custom editor.
* Value type is limited to primitives (`string | number | boolean`).
* For object and array values use {@link HotCellEditorAdvancedComponent}.
*/
class HotCellEditorComponent extends HotCellEditorBase {
static EDITOR_MARKER = Symbol('HotCellEditorComponent');
/** The tabindex attribute for the editor. */
tabindex = -1;
/** The data-hot-input attribute for the editor. */
dataHotInput = '';
/** The handsontableInput class for the editor. */
handsontableInputClass = true;
/** Event triggered by Handsontable on closing the editor. */
onClose() { }
/** Event triggered by Handsontable on opening the editor. */
onOpen(event) { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorComponent, decorators: [{
type: Directive
}], propDecorators: { tabindex: [{
type: HostBinding,
args: ['attr.tabindex']
}], dataHotInput: [{
type: HostBinding,
args: ['attr.data-hot-input']
}], handsontableInputClass: [{
type: HostBinding,
args: ['class.handsontableInput']
}] } });
/**
* Factory function to create a custom Handsontable editor adapter for Angular components.
*
* This adapter integrates Angular components with Handsontable's editor system using the new
* editorFactory API, allowing you to use Angular components as custom cell editors while
* maintaining full Angular lifecycle management and change detection.
*
* @returns A custom editor class that can be used in Handsontable column settings.
*
*/
const FactoryEditorAdapter = (componentRef) => editorFactory({
position: componentRef.instance.position,
shortcuts: componentRef.instance.shortcuts,
config: componentRef.instance.config,
init(editor) {
editor._componentRef = componentRef;
editor._editorPlaceHolderRef = undefined;
editor._finishEditSubscription = undefined;
editor._cancelEditSubscription = undefined;
createEditorPlaceholder(editor, editor.hot._angularEnvironmentInjector);
editor.input = editor._editorPlaceHolderRef?.location.nativeElement ?? document.createElement('div');
editor._afterRowResizeCallback = () => {
if (editor.isOpened()) {
applyPropsToEditor(editor);
}
};
editor._afterColumnResizeCallback = () => {
if (editor.isOpened()) {
applyPropsToEditor(editor);
}
};
editor._afterDestroyCallback = () => {
cleanupSubscriptions(editor);
if (editor._editorPlaceHolderRef) {
editor._editorPlaceHolderRef.destroy();
}
};
// Hooks are automatically removed by Handsontable on table destroy
editor.hot.addHook('afterRowResize', editor._afterRowResizeCallback);
editor.hot.addHook('afterColumnResize', editor._afterColumnResizeCallback);
editor.hot.addHook('afterDestroy', editor._afterDestroyCallback);
},
afterInit: (editor) => editor._componentRef.instance.afterInit?.(editor),
beforeOpen: (editor, context) => {
cleanupSubscriptions(editor);
applyPropsToEditor(editor);
editor._finishEditSubscription = editor._componentRef.instance.finishEdit.pipe(take(1)).subscribe(() => {
editor.finishEditing();
});
editor._cancelEditSubscription = editor._componentRef.instance.cancelEdit.pipe(take(1)).subscribe(() => {
editor.cancelChanges();
});
editor._componentRef.instance.beforeOpen?.(editor, context);
},
afterOpen: (editor, event) => {
editor._componentRef.instance.afterOpen?.(editor, event);
},
onFocus: (editor) => editor._componentRef.instance.onFocus?.(editor),
afterClose: (editor) => {
resetEditorState(editor);
editor._editorPlaceHolderRef?.changeDetectorRef.detectChanges();
editor._editorPlaceHolderRef?.instance.detachEditor();
editor._componentRef.instance.afterClose?.(editor);
},
getValue: (editor) => editor._componentRef.instance.getValue(),
setValue: (editor, value) => {
editor.value = value;
editor._componentRef.instance.setValue(value);
editor._componentRef.changeDetectorRef.detectChanges();
},
});
/**
* Creates the editor placeholder component.
* @param editor The editor instance.
* @param injector The environment injector from Angular.
*/
function createEditorPlaceholder(editor, injector) {
if (!injector) {
return;
}
editor._editorPlaceHolderRef = createComponent(CustomEditorPlaceholderComponent, {
environmentInjector: injector,
});
}
/**
* Applies properties to the custom Angular editor and editor placeholder.
* Updates position, size, and cell context information.
* @param editor The editor instance.
*/
function applyPropsToEditor(editor) {
if (!editor._componentRef || !editor._editorPlaceHolderRef) {
return;
}
editor._componentRef.setInput('originalValue', editor.originalValue);
editor._componentRef.setInput('row', editor.row);
editor._componentRef.setInput('column', editor.col);
editor._componentRef.setInput('prop', editor.prop);
editor._componentRef.setInput('cellProperties', editor.cellProperties);
const rect = editor.hot.getCell(editor.row, editor.col)?.getBoundingClientRect();
editor._editorPlaceHolderRef.setInput('placeholderCustomClass', '');
editor._editorPlaceHolderRef.setInput('height', rect?.height ?? 0);
editor._editorPlaceHolderRef.setInput('width', rect?.width ?? 0);
editor._editorPlaceHolderRef.setInput('isVisible', true);
editor._editorPlaceHolderRef.setInput('componentRef', editor._componentRef);
editor._editorPlaceHolderRef.changeDetectorRef.detectChanges();
}
/**
* Resets the editor placeholder state.
* Clears all positioning and visibility settings.
* @param editor The editor instance.
*/
function resetEditorState(editor) {
if (!editor._editorPlaceHolderRef) {
return;
}
editor._editorPlaceHolderRef.setInput('top', undefined);
editor._editorPlaceHolderRef.setInput('left', undefined);
editor._editorPlaceHolderRef.setInput('height', undefined);
editor._editorPlaceHolderRef.setInput('width', undefined);
editor._editorPlaceHolderRef.setInput('isVisible', false);
editor._editorPlaceHolderRef.setInput('componentRef', undefined);
}
/**
* Cleans up existing subscriptions.
* @param editor The editor instance.
*/
function cleanupSubscriptions(editor) {
if (editor._finishEditSubscription) {
editor._finishEditSubscription.unsubscribe();
editor._finishEditSubscription = undefined;
}
if (editor._cancelEditSubscription) {
editor._cancelEditSubscription.unsubscribe();
editor._cancelEditSubscription = undefined;
}
}
/**
* Abstract base component for creating advanced custom cell renderer components for Handsontable.
*
* Extend this component and provide your own template to implement a custom renderer.
* Unlike {@link HotCellRendererComponent}, this variant also accepts object and array values.
*
* @template TValue - The type of the component renderer.
* @template TProps - The type of additional renderer properties.
*/
class HotCellRendererAdvancedComponent extends HotCellRendererBase {
static RENDERER_MARKER = Symbol('HotCellRendererAdvancedComponent');
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererAdvancedComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellRendererAdvancedComponent, decorators: [{
type: Component,
args: [{
selector: 'hot-cell-renderer-advanced',
template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
}]
}] });
/**
* Abstract class representing an advanced Handsontable cell editor in Angular.
*
* Extend this class and decorate the subclass with `@Component()` to implement a custom editor.
* Unlike {@link HotCellEditorComponent}, this variant also accepts object and array values
* and provides additional lifecycle hooks and positioning options.
*/
class HotCellEditorAdvancedComponent extends HotCellEditorBase {
static EDITOR_MARKER = Symbol('HotCellEditorAdvancedComponent');
/** Event triggered by Handsontable on focusing the editor. Available in advanced mode. */
onFocus(editor) { }
/** The position of the editor in the DOM. Available in advanced mode. */
position = 'container';
/** The shortcuts available for the editor. Available in advanced mode. */
shortcuts;
/** The group name for the shortcuts. Available in advanced mode. */
shortcutsGroup;
/** Configuration object. Available in advanced mode. */
config;
/** Lifecycle hook called after the editor is opened. Available in advanced mode. */
afterOpen(editor, event) { }
/** Lifecycle hook called after the editor is closed. Available in advanced mode. */
afterClose(editor) { }
/** Lifecycle hook called after the editor is initialized. Available in advanced mode. */
afterInit(editor) { }
/** Lifecycle hook called before the editor is opened. Available in advanced mode. */
beforeOpen(editor, { row, col, prop, td, originalValue, cellProperties, }) { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorAdvancedComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.25", type: HotCellEditorAdvancedComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HotCellEditorAdvancedComponent, decorators: [{
type: Directive
}] });
const INVALID_RENDERER_WARNING = 'The provided renderer component was not recognized as a valid custom renderer. ' +
'It must either extend HotCellRendererComponent or be a valid TemplateRef. ' +
'Please ensure that your custom renderer is implemented correctly and imported from the proper source.';
const INVALID_ADVANCED_RENDERER_WARNING = 'The provided renderer component was not recognized as a valid custom renderer. ' +
'It must either extend HotCellRendererAdvancedComponent. ' +
'Please ensure that your custom renderer is implemented correctly and imported from the proper source.';
// Renderer component inputs, listed once at module scope so the per-cell render path can set them
// without allocating a fresh key array (via Object.keys) on every cell of every render frame.
const RENDERER_INPUT_KEYS = [
'instance', 'td', 'row', 'col', 'prop', 'value', 'cellProperties',
];
/**
* Type guard that checks if the given object is a TemplateRef.
*
* @param obj - The object to check.
* @returns True if the object is a TemplateRef; otherwise, false.
*/
function isTemplateRef(obj) {
return !!obj && typeof obj.createEmbeddedView === 'function';
}
/**
* Type guard to check if an object is an instance of HotCellRendererComponent.
*
* @param obj - The object to check.
* @returns True if the object is a HotCellRendererComponent, false otherwise.
*/
function isHotCellRendererComponent(obj) {
return obj?.RENDERER_MARKER === HotCellRendererComponent.RENDERER_MARKER;
}
/**
* Type guard to check if an object is an instance of HotCellRendererAdvancedComponent.
*
* @param obj - The object to check.
* @returns True if the object is a HotCellRendererAdvancedComponent, false otherwise.
*/
function isAdvancedHotCellRendererComponent(obj) {
return obj?.RENDERER_MARKER === HotCellRendererAdvancedComponent.RENDERER_MARKER;
}
/**
* Service for dynamically creating Angular components or templates as custom renderers for Handsontable.
*
* This service allows you to create a renderer function that wraps a given Angular component or TemplateRef
* so that it can be used as a Handsontable renderer.
*
* @example
* const customRenderer = dynamicComponentService.createRendererFromComponent(MyRendererComponent, { someProp: value });
* // Use customRenderer in your Handsontable configuration
*/
class DynamicComponentService {
appRef;
environmentInjector;
// Track Angular component refs and embedded views keyed by the TD element they are attached to.
// When a cell is re-rendered the previous component is destroyed before a new one is created.
_tdComponentRefs = new WeakMap();
_tdEmbeddedViews = new WeakMap();
// Per-instance registries of every renderer ref/view currently attached to the application.
// The WeakMaps above only allow per-TD lookup; these sets let us sweep refs whose TD was dropped
// from Handsontable's virtual viewport (scrolling, updateData), which would otherwise stay
// attached to ApplicationRef forever and leak both memory and change-detection work.
//
// The registries are scoped per Handsontable instance (not one global set) because this service
// is a root singleton shared by every <hot-table>. A global sweep would scan the cells of every
// table on the page on each `afterViewRender`, i.e. on every scroll frame of any one of them.
// Keying by instance bounds each sweep to the cells of the table that actually re-rendered.
_instanceComponentRefs = new WeakMap();
_instanceEmbeddedViews = new WeakMap();
// Instances we already wired the sweep hook into, so each instance is hooked at most once.
_hookedInstances = new WeakSet();
constructor(appRef, environmentInjector) {
this.appRef = appRef;
this.environmentInjector = environmentInjector;
}
/**
* Creates a custom renderer function for Handsontable from an Angular component or TemplateRef.
* The generated renderer function will be used by Handsontable to render cell content.
*
* @param component - The Angular component type or TemplateRef to use as renderer.
* @param componentProps - An object containing additional properties to use by the renderer.
* @param register - If true, registers the renderer with Handsontable using the component's name.
* @returns A renderer function that can be used in Handsontable's configuration.
*/
createRendererFromComponent(component, componentProps = {}, register = false) {
let registered = false;
return (instance, td, row, col, prop, value, cellProperties) => {
const properties = {
value,
instance,
td,
row,
col,
prop,
cellProperties,
};
cellProperties.rendererProps = componentProps;
baseRenderer.call(this, instance, td, row, col, prop, value, cellProperties);
this.registerSweepHook(instance);
if (isTemplateRef(component)) {
// Embedded views carry a per-render context, so they are always rebuilt.
this.replaceCellContent(instance, td);
const embeddedView = this.attachTemplateToElement(component, td, properties);
this.trackEmbeddedView(instance, td, embeddedView);
}
else if (isHotCellRendererComponent(component)) {
this.renderComponent(td, component, properties);
}
else {
console.warn(INVALID_RENDERER_WARNING);
}
if (register && !registered && isHotCellRendererComponent(component)) {
Handsontable.renderers.registerRenderer(component.name, component);
registered = true;
}
return td;
};
}
/**
* Creates a custom renderer function using rendererFactory from Handsontable.
* This is an alternative implementation that uses the factory pattern.
*
* @param component - The Angular component type to use as renderer.
* @param componentProps - An object containing additional properties to use by the renderer.
* @param register - If true, registers the renderer with Handsontable using the component's name.
* @returns A renderer function that can be used in Handsontable's configuration.
*/
createRendererWithFactory(component, componentProps = {}, register = false) {
let registered = false;
return rendererFactory(({ instance, td, row, column, prop, value, cellProperties }) => {
const properties = {
value,
instance,
td,
row,
col: column,
prop,
cellProperties,
};
cellProperties.rendererProps = componentProps;
// Apply the base renderer so the TD gets the same base classes/attributes as the
// createRendererFromComponent path (rendererFactory itself does not call it).
baseRenderer.call(this, instance, td, row, column, prop, value, cellProperties);
this.registerSweepHook(instance);
if (isAdvancedHotCellRendererComponent(component)) {
this.renderComponent(td, component, properties);
}
else {
console.warn(INVALID_ADVANCED_RENDERER_WARNING);
}
if (register && !registered && isAdvancedHotCellRendererComponent(component)) {
registerRenderer(component.name, component);
registered = true;
}
});
}
/**
* Destroys all renderer components and embedded views attached to cells within a container element.
* Must be called before destroying the Handsontable instance to prevent Angular component leaks.
*
* @param container - The root DOM element of the Handsontable instance.
* @param instance - The Handsontable instance whose registries should be torn down. When omitted
* (e.g. test stubs), only refs reachable through TDs still in the container are destroyed.
*/
cleanupContainer(container, instance) {
const compRefs = instance ? this._instanceComponentRefs.get(instance) : undefined;
const embViews = instance ? this._instanceEmbeddedViews.get(instance) : undefined;
container.querySelectorAll('td').forEach((td) => {
const compRef = this._tdComponentRefs.get(td);
if (compRef) {
this.destroyComponent(compRef);
this._tdComponentRefs.delete(td);
compRefs?.delete(compRef);
}
const embView = this._tdEmbeddedViews.get(td);
if (embView) {
this.destroyEmbeddedView(embView);
this._tdEmbeddedViews.delete(td);
embViews?.delete(embView);
}
});
// The loop above only reaches TDs still present in the container. Refs for cells already
// dropped from the viewport (but not yet swept) would otherwise be orphaned once this
// instance's afterViewRender hook is gone after destroy. Tear down whatever is left and drop
// the per-instance registries so a repeated cleanup call is a no-op. (Entries removed in the
// loop above are already gone from these sets, so nothing is destroyed twice.)
compRefs?.forEach((ref) => this.destroyComponent(ref));
embViews?.forEach((view) => this.destroyEmbeddedView(view));
if (instance) {
this._instanceComponentRefs.delete(instance);
this._instanceEmbeddedViews.delete(instance);
}
}
/**
* Registers a one-time `afterViewRender` hook on the given Handsontable instance that sweeps
* renderer refs whose TD is no longer connected to the document. Handsontable recycles a pool of
* TD elements while virtualizing rows; cells that leave the viewport are never re-rendered, so
* without this sweep their Angular components stay attached to ApplicationRef and leak.
*
* Guarded against instances that do not expose `addHook` (e.g. test stubs).
*
* @param instance - The Handsontable instance whose render cycle drives the sweep.
*/
registerSweepHook(instance) {
if (this._hookedInstances.has(instance) || typeof instance?.addHook !== 'function') {
return;
}
this._hookedInstances.add(instance);
instance.addHook('afterViewRender', () => this.sweepDetachedViews(instance));
}
/**
* Destroys every renderer ref/view tracked for the given instance whose root node is no longer
* attached to the document.
*
* @param instance - The Handsontable instance whose registries should be swept.
*/
sweepDetachedViews(instance) {
const compRefs = this._instanceComponentRefs.get(instance);
compRefs?.forEach((ref) => {
if (!this.isViewConnected(ref.hostView)) {
this.destroyComponent(ref);
compRefs.delete(ref);
}
});
const embViews = this._instanceEmbeddedViews.get(instance);
embViews?.forEach((view) => {
if (!this.isViewConnected(view)) {
this.destroyEmbeddedView(view);
embViews.delete(view);
}
});
}
/**
* @returns True if any of the view's root nodes is still connected to the document.
*/
isViewConnected(view) {
return view.rootNodes.some((node) => !!node?.isConnected);
}
/**
* Destroys the renderer ref/view previously attached to a cell and clears the cell content,
* so a fresh renderer can take over the TD without leaking the old one.
*
* @param instance - The Handsontable instance owning the cell, used to update its registries.
* @param td - The table cell whose previous content should be torn down.
*/
replaceCellContent(instance, td) {
const prevRef = this._tdComponentRefs.get(td);
if (prevRef) {
this.destroyComponent(prevRef);
this._tdComponentRefs.delete(td);
this._instanceComponentRefs.get(instance)?.delete(prevRef);
}
const prevView = this._tdEmbeddedViews.get(td);
if (prevView) {
this.destroyEmbeddedView(prevView);
this._tdEmbeddedViews.delete(td);
this._instanceEmbeddedViews.get(instance)?.delete(prevView);
}
td.innerHTML = '';
}
/**
* Tracks a component ref both by its TD (for fast replacement) and in the instance registry
* (for sweeping and full teardown).
*/
trackComponentRef(instance, td, ref) {
this._tdComponentRefs.set(td, ref);
this.registryFor(this._instanceComponentRefs, instance).add(ref);
}
/**
* Tracks an embedded view both by its TD (for fast replacement) and in the instance registry
* (for sweeping and full teardown).
*/
trackEmbeddedView(instance, td, view) {
this._tdEmbeddedViews.set(td, view);
this.registryFor(this._instanceEmbeddedViews, instance).add(view);
}
/**
* Returns the per-instance registry set for the given instance, creating it on first use.
*/
registryFor(registry, instance) {
let set = registry.get(instance);
if (!set) {
set = new Set();
registry.set(instance, set);
}
return set;
}
/**
* Attaches an embedded view created from a TemplateRef to a given DOM element.
*
* @param template - The TemplateRef to create an embedded view from.
* @param tdEl - The target DOM element (a table cell) to which the view will be appended.
* @param properties - Context object providing properties to be used within the template.
* @returns The created EmbeddedViewRef so the caller can track and destroy it later.
*/
attachTemplateToElement(template, tdEl, properties) {
const embeddedView = template.createEmbeddedView({
$implicit: properties.value,
...properties,
});
embeddedView.detectChanges();
embeddedView.rootNodes.forEach((node) => {
tdEl.appendChild(node);
});
return embeddedView;
}
/**
* Dynamically creates an Angular component of the given type.
*
* @param component - The Angular component type to be created.
* @param rendererParameters - An object containing input properties to assign to the component instance.
* @returns The ComponentRef of the dynamically created component.
*/
createComponent(component, rendererParameters) {
const componentRef = createComponent(component, {
environmentInjector: this.environmentInjector,
});
this.applyInputs(componentRef, rendererParameters);
componentRef.changeDetectorRef.detectChanges();
this.appRef.attachView(componentRef.hostView);
return componentRef;
}
/**
* Renders an Angular component into the given cell, recycling the component already attached to
* the TD when it is of the same type. Handsontable recycles its pool of TD elements heavily while
* virtualizing rows, so recreating an Angular component on every re-render would cause needless
* teardown/instantiation churn and GC pressure. When the type matches we only refresh the inputs.
*
* @param td - The target table cell.
* @param component - The renderer component type to render.
* @param properties - The renderer parameters to feed as component inputs.
*/
renderComponent(td, component, properties) {
const prevRef = this._tdComponentRefs.get(td);
if (prevRef &&
prevRef.componentType === component &&
this.isViewConnected(prevRef.hostView)) {
this.applyInputs(prevRef, properties);
prevRef.changeDetectorRef.detectChanges();
return;
}
this.replaceCellContent(properties.instance, td);
const componentRef = this.createComponent(component, properties);
this.attachComponentToElement(componentRef, td);
this.trackComponentRef(properties.instance, td, componentRef);
}
/**
* Assigns every renderer parameter as an input on the given component ref.
*
* @param componentRef - The component ref whose inputs should be set.
* @param rendererParameters - The renderer parameters to assign.
*/
applyInputs(componentRef, rendererParameters) {
RENDERER_INPUT_KEYS.forEach((key) => {
componentRef.setInput(key, rendererParameters[key]);
});
}
/**
* Attaches a dynamically created component's view to a specified DOM container element.
*
* @param componentRef - The reference to the dynamically created component.
* @param container - The target DOM element to which the component's root node will be appended.
*/
attachComponentToElement(componentRef, container) {
componentRef.hostView.rootNodes.forEach((node) => {
container.appendChild(node);
});
}
/**
* Destroys a dynamically created component and detaches its view from the Angular application.
*
* Idempotent: a TD recycled after `sweepDetachedViews` already destroyed its ref still maps to that
* stale ref in `_tdComponentRefs`, so the next render path may reach this with an already-destroyed
* ref. Guarding on `destroyed` skips the redundant detach/destroy instead of relying on Angular's
* internal no-op behaviour.
*
* @param componentRef - The reference to the component to be destroyed.
*/
destroyComponent(componentRef) {
const hostView = componentRef.hostView;
if (hostView.destroyed) {
return;
}
this.appRef.detachView(hostView);
componentRef.destroy();
}
/**
* Destroys an embedded view. Idempotent for the same reason as {@link destroyComponent}: a recycled
* TD can still map to an already-destroyed view in `_tdEmbeddedViews`.
*
* @param view - The embedded view to destroy.
*/
destroyEmbeddedView(view) {
if (view.destroyed) {
return;
}
view.destroy();
}
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 });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicComponentService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicComponentService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }] });
const AVAILABLE_HOOKS_SET = new Set(Handsontable.hooks.getRegistered());
const HOT_ZONE_WRAPPED = Symbol('hotZoneWrapped');
/**
* Service to resolve and apply custom settings for Handsontable settings object.
*/
class HotSettingsResolver {
dynamicComponentService;
environmentInjector;
ngZone;
constructor(dynamicComponentService, environmentInjector, ngZone) {
this.dynamicComponentService = dynamicComponentService;
this.environmentInjector = environmentInjector;
this.ngZone = ngZone;
}
/**
* Applies custom settings to the provided GridSettings.
* @param settings The original grid settings.
* @param previousColumns The previously resolved columns (from the prior settings cycle). When
* supplied, an editor component already created for a column whose editor type is unchanged is
* recycled instead of being recreated, avoiding needless Angular component teardown/rebuild.
* @returns The merged grid settings with custom settings applied.
*/
applyCustomSettings(settings, previousColumns) {
// Shallow-clone the user settings (and each column) before mutating. Otherwise we would
// write generated renderers/editors and `_editorComponentReference` straight onto the
// caller's objects. When the same settings/columns are shared across two <hot-table>
// instances, the second resolution would overwrite the first instance's editor refs,
// leaking them and cross-wiring a single editor component between tables.
const mergedSettings = { ...settings };
if (Array.isArray(mergedSettings.columns)) {
mergedSettings.columns = mergedSettings.columns.map((column) => ({ ...column }));
}
this.updateColumnRendererForGivenCustomRenderer(mergedSettings);
this.updateColumnEditorForGivenCustomEditor(mergedSettings, previousColumns);
this.updateColumnValidatorForGivenCustomValidator(mergedSettings);
this.wrapHooksInNgZone(mergedSettings);
return mergedSettings;
}
/**
* Ensures that hook callbacks in the provided grid settings run inside Angular's zone.
*
* @param settings The original grid settings.
*/
wrapHooksInNgZone(settings) {
const ngZone = this.ngZone;
// Iterate only the keys actually present in settings instead of all ~100 registered HOT hooks.
Object.keys(settings).forEach((key) => {
if (!AVAILABLE_HOOKS_SET.has(key)) {
return;
}
const option = settings[key];
if (typeof option === 'functio