Cell editor
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 }],};/* 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 modulesregisterAllModules();
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 */<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.
/* 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 modulesregisterAllModules();
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 */<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:
| Step | When it runs | What it does |
|---|---|---|
| Select editor | Cell is selected | Looks up the editor class from the editor config option |
| Prepare | Cell is selected | Calls prepare() to configure the editor for the selected cell |
| Open | User triggers editing | Fires beforeBeginEditing (return false to cancel), then calls beginEditing() → open() |
| Close | User confirms or cancels | Calls 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 / action | Result |
|---|---|
| Click another cell | Saves changes |
| Enter / Shift+Enter | Saves and moves selection down / up |
| Tab / Shift+Tab | Saves and moves selection right / left |
| Ctrl/Cmd+Enter or Alt/Option+Enter | Inserts a line break |
| Page Up / Page Down | Saves and scrolls one screen |
| Escape | Cancels 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.
| Method | Description |
|---|---|
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.
| Method | Description |
|---|---|
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.
| Property | Type | Description |
|---|---|---|
hot | Handsontable.Core | The Handsontable instance this editor belongs to. Set once in the constructor; never changes. |
row | number | Visual row index of the active cell. Updated by prepare(). |
col | number | Visual column index of the active cell. Updated by prepare(). |
prop | string | Data property name for the active cell (for object data sources). Updated by prepare(). |
TD | HTMLTableCellElement | DOM node of the active cell. Updated by prepare(). |
cellProperties | object | Configuration of the active cell. Updated by prepare(). Includes all standard cell options and any custom properties (e.g., selectOptions). |
Built-in editors
| Alias | Editor class |
|---|---|
autocomplete | Handsontable.editors.AutocompleteEditor |
checkbox | Handsontable.editors.CheckboxEditor |
date | Handsontable.editors.DateEditor |
dropdown | Handsontable.editors.DropdownEditor |
handsontable | Handsontable.editors.HandsontableEditor |
numeric | Handsontable.editors.NumericEditor |
password | Handsontable.editors.PasswordEditor |
select | Handsontable.editors.SelectEditor |
text | Handsontable.editors.TextEditor |
time | Handsontable.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
BaseEditordirectly 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.
/* 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 modulesregisterAllModules();
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 */<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 inprepare()oropen().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 viaselectOptions.open()/close()- ThebeforeKeyDownhook 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}Related keyboard shortcuts
| Windows | macOS | Action | Excel | Sheets |
|---|---|---|---|---|
| Arrow keys | Arrow keys | Move the cursor through the text | ✓ | ✓ |
| Alphanumeric keys | Alphanumeric keys | Enter the pressed key’s value into the cell | ✓ | ✓ |
| Enter | Enter | Complete the cell entry and move to the cell below | ✓ | ✓ |
| Shift+Enter | Shift+Enter | Complete the cell entry and move to the cell above | ✓ | ✓ |
| Tab | Tab | Complete the cell entry and move to the next cell* | ✓ | ✓ |
| Shift+Tab | Shift+Tab | Complete the cell entry and move to the previous cell* | ✓ | ✓ |
| Delete | Delete | Delete one character after the cursor* | ✓ | ✓ |
| Backspace | Backspace | Delete one character before the cursor* | ✓ | ✓ |
| Home | Home | Move the cursor to the beginning of the text* | ✓ | ✓ |
| End | End | Move the cursor to the end of the text* | ✓ | ✓ |
| Ctrl + Arrow keys | Cmd + Arrow keys | Move the cursor to the beginning or to the end of the text | ✓ | ✓ |
| Ctrl+Shift + Arrow keys | Cmd+Shift + Arrow keys | Extend the selection to the beginning or to the end of the text | ✓ | ✓ |
| Page Up | Page Up | Complete the cell entry and move one screen up | ✓ | ✓ |
| Page Down | Page Down | Complete the cell entry and move one screen down | ✓ | ✓ |
| Alt+Enter | Option+Enter | Insert a line break | ✗ | ✓ |
| Ctrl+Enter | Ctrl/Cmd+Enter | Insert a line break | ✗ | ✓ |
| Escape | Escape | Cancel the cell entry and exit the editing mode | ✓ | ✓ |
* This action depends on your layout direction.
Related API reference
APIs
Configuration options
Core methods
Hooks