Skip to content

Cell editor

Create custom cell editors to fully control how values are entered in your data grid.

Each cell can have one editor — a class that manages the editor’s DOM element, its value, and the lifecycle from opening to saving. Handsontable’s EditorManager selects and drives the editor automatically. You create editors by extending BaseEditor or any of the built-in editor classes.

Component-based editors

Create an Angular component that extends HotCellEditorComponent<T> (where T is string, number, or boolean). The only method you must implement is onFocus(). Use the finishEdit and cancelEdit outputs to trigger editing completion.

@Component({
selector: 'app-custom-editor',
imports: [FormsModule],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<input #inputElement type="text" [value]="getValue()" (keydown)="onKeyDown($event)" />
</div>
`,
})
export class CustomEditorComponent extends HotCellEditorComponent<string> {
@ViewChild('inputElement') inputElement!: ElementRef;
onFocus(): void {
this.inputElement.nativeElement.select();
}
}

Pass the component as the editor property in column configuration:

settings = {
columns: [{ editor: CustomEditorComponent }],
};
TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings, HotCellEditorComponent } from '@handsontable/angular-wrapper';
@Component({
standalone: false,
template: `<button (click)="toUpperCase()">Upper</button>
<button (click)="toLowerCase()">Lower</button>`,
})
export class EditorComponent extends HotCellEditorComponent<string> {
override onFocus(): void {}
toUpperCase(): void {
this.setValue(this.getValue().toUpperCase());
this.finishEdit.emit();
}
toLowerCase(): void {
this.setValue(this.getValue().toLowerCase());
this.finishEdit.emit();
}
}
@Component({
selector: 'example1-cell-editor',
standalone: false,
template: ` <div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>`,
})
export class Example1CellEditorComponent {
readonly data = [
['Obrien Fischer'],
['Alexandria Gordon'],
['John Stafford'],
['Regina Waters'],
['Kay Bentley'],
['Emerson Drake'],
['Dean Stapleton'],
];
readonly gridSettings: GridSettings = {
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
columns: [
{
width: 250,
editor: EditorComponent,
},
]
};
}
/* end-file */
/* file: app.module.ts */
import { NgModule, ApplicationConfig } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from '@handsontable/angular-wrapper';
import { CommonModule } from '@angular/common';
import { NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
/* start:skip-in-compilation */
import { Example1CellEditorComponent, EditorComponent } from './app.component';
/* end:skip-in-compilation */
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HOT_GLOBAL_CONFIG,
useValue: {
license: NON_COMMERCIAL_LICENSE,
} as HotGlobalConfig
}
],
};
@NgModule({
imports: [ BrowserModule, HotTableModule, CommonModule ],
declarations: [ Example1CellEditorComponent, EditorComponent ],
providers: [...appConfig.providers],
bootstrap: [ Example1CellEditorComponent ]
})
export class AppModule { }
/* end-file */
HTML
<div>
<example1-cell-editor></example1-cell-editor>
</div>

Class-based editors

Declare the editor as a class that extends a built-in editor (e.g., TextEditor) and pass it to the editor option in column configuration.

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings } from '@handsontable/angular-wrapper';
import { TextEditor } from 'handsontable/editors';
class CustomEditor extends TextEditor {
override createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
@Component({
selector: 'example2-cell-editor',
standalone: false,
template: ` <div>
<hot-table [settings]="gridSettings"></hot-table>
</div>`,
})
export class Example2CellEditorComponent {
readonly gridSettings: GridSettings = {
colHeaders: true,
startRows: 5,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
colWidths: 200,
columns: [
{
editor: CustomEditor,
},
]
};
}
/* end-file */
/* file: app.module.ts */
import { NgModule, ApplicationConfig } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from '@handsontable/angular-wrapper';
import { CommonModule } from '@angular/common';
import { NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
/* start:skip-in-compilation */
import { Example2CellEditorComponent } from './app.component';
/* end:skip-in-compilation */
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HOT_GLOBAL_CONFIG,
useValue: {
license: NON_COMMERCIAL_LICENSE,
} as HotGlobalConfig
}
],
};
@NgModule({
imports: [ BrowserModule, HotTableModule, CommonModule ],
declarations: [ Example2CellEditorComponent ],
providers: [...appConfig.providers],
bootstrap: [ Example2CellEditorComponent ]
})
export class AppModule { }
/* end-file */
HTML
<div>
<example2-cell-editor></example2-cell-editor>
</div>

How editing works

Handsontable separates rendering (displaying cell values) from editing (changing them). Renderers are stateless functions; editors are stateful classes that own a DOM element and manage the full editing flow.

EditorManager lifecycle

EditorManager orchestrates all editors. For each cell interaction it runs four steps in order:

StepWhen it runsWhat it does
Select editorCell is selectedLooks up the editor class from the editor config option
PrepareCell is selectedCalls prepare() to configure the editor for the selected cell
OpenUser triggers editingFires beforeBeginEditing (return false to cancel), then calls beginEditing()open()
CloseUser confirms or cancelsCalls finishEditing()close() or focus()

Editing opens on: Enter, Shift+Enter, F2, double-click.

Preventing the editor from opening: Use the beforeBeginEditing hook to conditionally cancel editor opening. Return false from the hook callback to prevent the editor from opening. Returning undefined (or any non-boolean value) applies the default behavior, which disallows opening for non-contiguous selections (Ctrl/Cmd+click) and multi-cell selections (Shift+click). Returning true removes those restrictions.

Editing closes on:

Key / actionResult
Click another cellSaves changes
Enter / Shift+EnterSaves and moves selection down / up
Tab / Shift+TabSaves and moves selection right / left
Ctrl/Cmd+Enter or Alt/Option+EnterInserts a line break
Page Up / Page DownSaves and scrolls one screen
EscapeCancels without saving

Overriding keyboard behavior: Register a beforeKeyDown hook listener and call event.stopImmediatePropagation() to prevent EditorManager from processing a specific key. Register the listener in open() and remove it in close() so it only intercepts events while your editor is active.

Editor singleton: Each editor class has exactly one instance per table. The constructor and init() run once per table; prepare() runs every time the user selects a cell that uses that editor.

BaseEditor API

All custom editors extend Handsontable.editors.BaseEditor. In an ESM project, import it from handsontable/editors/baseEditor. In a script-tag (UMD) project, access it as Handsontable.editors.BaseEditor.

Common methods

These methods are already implemented in BaseEditor. Override them only when necessary — always call super.method() first when you do.

MethodDescription
prepare()Configures the editor for the selected cell. Sets this.row, this.col, this.prop, this.TD, and this.cellProperties. Parameters: (row, col, prop, td, originalValue, cellProperties).
beginEditing()Sets the initial editor value and calls open(). Uses the original cell value when newInitialValue is undefined. Parameters: (newInitialValue, event).
finishEditing()Validates and saves the value, or restores the original when restoreOriginalValue is true. When ctrlDown is true, applies the value to all selected cells. Calls close() on success or focus() when validation fails. Parameters: (restoreOriginalValue?, ctrlDown?, callback?).
discardEditor()Called after validation. If valid, calls close(); if invalid, calls focus() to keep the editor open. Parameter: (result).
saveValue()Validates and saves value. Applies to all selected cells when ctrlDown is true. Parameters: (value, ctrlDown).
isOpened()Returns true if the editor is currently open.
extend()Returns a new subclass of the current editor. Overriding methods on the returned class does not affect the original. Useful for ES5 environments; in ES6+ prefer class MyEditor extends BaseEditor {}.

Abstract methods

You must implement all of these in your custom editor class.

MethodDescription
init()Called once when the editor object is created. Build the editor’s DOM here.
getValue()Returns the current editor value to be saved as the new cell value.
setValue()Updates the editor to display the new value. Parameter: (newValue).
open()Shows the editor.
close()Hides the editor after editing ends.
focus()Focuses the editor input. Called when validation fails and the editor must stay open.

Editor instance properties

Available as this.property inside any editor method.

PropertyTypeDescription
hotHandsontable.CoreThe Handsontable instance this editor belongs to. Set once in the constructor; never changes.
rownumberVisual row index of the active cell. Updated by prepare().
colnumberVisual column index of the active cell. Updated by prepare().
propstringData property name for the active cell (for object data sources). Updated by prepare().
TDHTMLTableCellElementDOM node of the active cell. Updated by prepare().
cellPropertiesobjectConfiguration of the active cell. Updated by prepare(). Includes all standard cell options and any custom properties (e.g., selectOptions).

Built-in editors

AliasEditor class
autocompleteHandsontable.editors.AutocompleteEditor
checkboxHandsontable.editors.CheckboxEditor
dateHandsontable.editors.DateEditor
dropdownHandsontable.editors.DropdownEditor
handsontableHandsontable.editors.HandsontableEditor
numericHandsontable.editors.NumericEditor
passwordHandsontable.editors.PasswordEditor
selectHandsontable.editors.SelectEditor
textHandsontable.editors.TextEditor
timeHandsontable.editors.TimeEditor

Creating a custom editor

Two approaches are available:

  • Extend an existing editor when you want most of its behavior and only need to change a few details (e.g., swap the input element type).
  • Extend BaseEditor directly when you need an entirely different UI (e.g., a dropdown list, a date picker, a color picker).

Extending an existing editor

Override only the methods you need. The PasswordEditor below extends TextEditor and replaces the <textarea> with <input type="password">. Since both elements share the same DOM API, only createElements() needs to change.

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings } from '@handsontable/angular-wrapper';
import { TextEditor } from 'handsontable/editors';
class CustomEditor extends TextEditor {
override createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
@Component({
selector: 'example2-cell-editor',
standalone: false,
template: ` <div>
<hot-table [settings]="gridSettings"></hot-table>
</div>`,
})
export class Example2CellEditorComponent {
readonly gridSettings: GridSettings = {
colHeaders: true,
startRows: 5,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
colWidths: 200,
columns: [
{
editor: CustomEditor,
},
]
};
}
/* end-file */
/* file: app.module.ts */
import { NgModule, ApplicationConfig } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from '@handsontable/angular-wrapper';
import { CommonModule } from '@angular/common';
import { NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
/* start:skip-in-compilation */
import { Example2CellEditorComponent } from './app.component';
/* end:skip-in-compilation */
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HOT_GLOBAL_CONFIG,
useValue: {
license: NON_COMMERCIAL_LICENSE,
} as HotGlobalConfig
}
],
};
@NgModule({
imports: [ BrowserModule, HotTableModule, CommonModule ],
declarations: [ Example2CellEditorComponent ],
providers: [...appConfig.providers],
bootstrap: [ Example2CellEditorComponent ]
})
export class AppModule { }
/* end-file */
HTML
<div>
<example2-cell-editor></example2-cell-editor>
</div>

Building an editor from scratch

Extend BaseEditor directly for full control. The SelectEditor below renders a <select> dropdown. It also overrides keyboard behavior so Arrow Up / Arrow Down cycle through options rather than closing the editor.

Key implementation decisions:

  • init() - DOM is created here (runs once), not in prepare() or open().
  • data-hot-input="true" - Required on the editor element. Without it, Handsontable treats clicks on the editor as clicks outside it and closes the editor immediately.
  • prepare() - Options are populated here so each cell can have its own list via selectOptions.
  • open() / close() - The beforeKeyDown hook is registered when opening and cleared when closing, scoping key interception to this editor only.

To use a class-based editor from scratch in Angular, pass the editor class as the editor property in a column configuration object. See the class-based editors section for an example.

Registering an editor

Register your editor under a string alias so it can be referenced by name in configuration instead of passing the class directly:

Handsontable.editors.registerEditor('myEditor', MyEditor);

After registration, use the alias in column configuration:

settings = { columns: [{ editor: 'myEditor' }] };
<hot-table [settings]="settings" />

Handsontable defines these aliases by default: autocomplete, base, checkbox, date, dropdown, handsontable, numeric, password, select, text, time.

Choose a unique alias (e.g., 'myorg.select') to avoid overwriting built-in editors. Registering under an existing alias silently overwrites the previous mapping.

Publishing a custom editor

If you intend to distribute your editor as a library, two extra steps make it easy for others to discover and extend it.

Step 1: Register the alias (covered above) so users can reference the editor by name:

Handsontable.editors.registerEditor('myorg.select', MySelectEditor);

Step 2: Expose the class on Handsontable.editors so users can retrieve and extend it without importing from your source module:

Handsontable.editors.MySelectEditor = MySelectEditor;

To extend a published editor, retrieve it with getEditor():

const MySelectEditor = Handsontable.editors.getEditor('myorg.select');
class ExtendedSelectEditor extends MySelectEditor {
// override methods as needed
}
WindowsmacOSActionExcelSheets
Arrow keysArrow keysMove the cursor through the text
Alphanumeric keysAlphanumeric keysEnter the pressed key’s value into the cell
EnterEnterComplete the cell entry and move to the cell below
Shift+EnterShift+EnterComplete the cell entry and move to the cell above
TabTabComplete the cell entry and move to the next cell*
Shift+TabShift+TabComplete the cell entry and move to the previous cell*
DeleteDeleteDelete one character after the cursor*
BackspaceBackspaceDelete one character before the cursor*
HomeHomeMove the cursor to the beginning of the text*
EndEndMove the cursor to the end of the text*
Ctrl + Arrow keysCmd + Arrow keysMove the cursor to the beginning or to the end of the text
Ctrl+Shift + Arrow keysCmd+Shift + Arrow keysExtend the selection to the beginning or to the end of the text
Page UpPage UpComplete the cell entry and move one screen up
Page DownPage DownComplete the cell entry and move one screen down
Alt+EnterOption+EnterInsert a line break
Ctrl+EnterCtrl/Cmd+EnterInsert a line break
EscapeEscapeCancel the cell entry and exit the editing mode

* This action depends on your layout direction.

APIs

Configuration options

Core methods

Hooks