Angular Data GridSimplified Custom Cell Definitions
- Overview
- Why This Approach?
- Angular
- Custom Renderers
- Custom Editors
- Renderers
- What is editorFactory?
- Basic Usage
- Lifecycle Methods
- Custom Properties with TypeScript
- Common Patterns
- Pattern 1: Simple Input Wrapper
- Pattern 2: Third-Party Library Integration
- Pattern 3: Preventing Click-Outside Closing
- Pattern 4: Per-Cell Configuration
- Pattern 5: Keyboard Shortcuts
- Pattern 6: Overriding Editor Default Behavior
- Pattern 7: Using Direct Value and Config Properties
- Pattern 8: Custom Positioning Strategy
- Pattern 9: Organizing Keyboard Shortcuts
- Usage in Handsontable
- Best Practices with editorFactory
- Examples
- Migration from Traditional Approach
- Troubleshooting with editorFactory
- Contributing
Overview
This document introduces a convention-over-configuration (opens new window), declarative approach to creating custom cell types in Handsontable. Rather than relying on imperative code and complex class hierarchies, you start with a working cell definition—just a few lines of code—which you then adjust to your own needs. While the previous, class-based approach isn't inherently bad and remains valuable for advanced customizations, it can be unnecessarily complex for simple editors or quick prototypes. With this factory-based method, you get a much simpler and faster way to build custom cells, while still retaining full access to Handsontable features.
Why This Approach?
The traditional OOP approach to creating custom cells has several challenges:
- Steep Learning Curve: Requires understanding of EditorManager, BaseEditor lifecycle, and complex inheritance patterns
- Boilerplate Code: Lots of repetitive code for simple customizations
- Error-Prone: Easy to miss critical lifecycle methods or forget to call super methods
- Poor Developer Experience: Not optimized for modern workflows (AI assistance, quick prototyping)
- Functional programming patterns are increasingly popular - example React moved from class components to hooks specifically to avoid
thisconfusion
Our goal: Make custom cell creation so simple that any developer can create a custom cell in minutes with AI assistance.
Angular
For Angular applications, Handsontable provides HotCellEditorAdvancedComponent and HotCellRendererAdvancedComponent, high-level Angular components that simplify creating custom editors and renderers. These components handle lifecycle management, provide type-safe properties, and integrate seamlessly with Angular's dependency injection and change detection.
What are the Advanced Components?
Angular provides two base classes for creating custom cells:
HotCellRendererAdvancedComponent- Base class for custom cell renderersHotCellEditorAdvancedComponent- Base class for custom cell editors
Both components provide:
- Automatic lifecycle management
- Type-safe @Input and @Output properties
- Built-in keyboard shortcut support
- Integration with Angular's change detection
- Support for custom configuration via
rendererPropsorconfig
Custom Renderers
HotCellRendererAdvancedComponent
A base class for creating custom cell renderers in Angular. Extend this class to create your own renderer components.
Basic Structure
import { Component } from "@angular/core";
import { HotCellRendererAdvancedComponent } from "@handsontable/angular-wrapper";
@Component({
selector: "my-custom-renderer",
template: ` <div>{{ value }}</div> `,
standalone: false,
})
export class MyCustomRenderer extends HotCellRendererAdvancedComponent<string> {
// Your custom logic here
}
Input Properties
The base class provides the following @Input properties automatically:
value: TValue- The cell value (typed based on generic parameter)instance: Handsontable- Handsontable instancetd: HTMLTableCellElement- The cell's TD elementrow: number- Row indexcol: number- Column indexprop: string- Property namecellProperties: Handsontable.CellProperties & { rendererProps?: TProps }- Cell configuration with optional renderer-specific properties
The getProps() Method
Use getProps() to retrieve renderer-specific properties passed via rendererProps:
@Component({
selector: 'colored-renderer',
template: `
<div [style.color]="getProps().textColor">
{{ value }}
</div>
`,
standalone: false,
})
export class ColoredRenderer extends HotCellRendererAdvancedComponent<string, { textColor: string }> {
// getProps() automatically typed as { textColor: string }
}
// Usage in column configuration:
{
data: 'name',
renderer: ColoredRenderer,
rendererProps: { textColor: 'blue' }
}
Example: Star Rating Renderer
import { Component, ChangeDetectionStrategy } from "@angular/core";
import { HotCellRendererAdvancedComponent } from "@handsontable/angular-wrapper";
@Component({
selector: "star-renderer",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
@for (star of stars; track $index) {
<span [style.opacity]="$index < value ? '1' : '0.4'">⭐</span>
}
</div>
`,
standalone: false,
})
export class StarRenderer extends HotCellRendererAdvancedComponent<number> {
readonly stars = Array(5);
}
// Usage:
columns: [
{
data: "rating",
renderer: StarRenderer,
},
];
When should I use HotCellRendererComponent and when should I use HotCellRendererAdvancedComponent?
The choice between HotCellRendererComponent and HotCellRendererAdvancedComponent depends on the underlying Handsontable API you intend to use.
HotCellRendererComponent: This is the base component for creating renderers that are compatible with the baseRenderer API from Handsontable. It is suitable for most standard rendering use cases.
HotCellRendererAdvancedComponent: This component is designed to work with the newer rendererFactory API from Handsontable. While both components offer similar configuration options at the Angular level, HotCellRendererAdvancedComponent aligns with the more modern, factory-based approach in the core Handsontable library, which can be more optimized.
In short:
- Use
HotCellRendererComponentfor renderers based on the traditionalbaseRenderer. - Use
HotCellRendererAdvancedComponentwhen you want to align with the newerrendererFactorypattern for potential performance benefits and a more modern API approach.
Custom Editors
HotCellEditorAdvancedComponent
A base class for creating custom cell editors in Angular. Extend this class to create your own editor components with full control over the editing experience.
Basic Structure
import { Component } from "@angular/core";
import { HotCellEditorAdvancedComponent } from "@handsontable/angular-wrapper";
@Component({
selector: "my-custom-editor",
template: ` <input [(ngModel)]="value" (blur)="finishEdit.emit()" /> `,
standalone: false,
})
export class MyCustomEditor extends HotCellEditorAdvancedComponent<string> {
// Your custom logic here
}
Input Properties
The base class provides the following @Input properties:
row: number- Row index of the cell being editedcolumn: number- Column index of the cell being editedprop: string | number- Property name of the cell being editedoriginalValue: T- Original value of the cell before editingcellProperties: CellProperties- Cell configuration
Output Events
finishEdit: EventEmitter<void>- Emit to save changes and close the editorcancelEdit: EventEmitter<void>- Emit to cancel changes and revert to original value
Lifecycle Methods
Override these methods to customize editor behavior:
onFocus(editor?: ExtendedEditor<T>): void- Called when the editor receives focus
- Use for custom focus management
afterOpen(editor: ExtendedEditor<T>, event?: Event): void- Called after the editor is opened and positioned
- Use to open dropdowns, trigger animations, or focus elements
afterClose(editor: ExtendedEditor<T>): void- Called when the editor closes
- Use for cleanup actions
afterInit(editor: ExtendedEditor<T>): void- Called after the editor is initialized
- Use for one-time setup
beforeOpen(editor, { row, col, prop, td, originalValue, cellProperties }): void- Called before the editor opens
- Use for per-cell setup or reading custom properties
Value Methods
getValue(): T- Returns the current editor value (override if needed)setValue(value: T): void- Sets the editor value (override if needed)
Configuration Properties
position: 'container' | 'portal'- Editor positioning strategy (default: 'container')shortcuts?: KeyboardShortcutConfig[]- Keyboard shortcuts configurationshortcutsGroup?: string- Group name for shortcutsconfig?: any- Custom configuration object
When should I use HotCellEditorComponent and when should I use HotCellEditorAdvancedComponent?
The choice between HotCellEditorComponent and HotCellEditorAdvancedComponent depends on your cell editor requirements.
HotCellEditorComponent: This is the basic component for creating simple editors. It is based on the older BaseEditor from Handsontable and is ideal for simple use cases.
HotCellEditorAdvancedComponent: This component is built on the newer editorFactory API from Handsontable. It offers significantly more configuration and customization options, such as:
Defining custom keyboard shortcuts. Choosing the editor's positioning strategy ('container' or 'portal'). Access to additional lifecycle hooks like afterInit and beforeOpen.
In short:
- Use
HotCellEditorComponentfor simple, standard editors. - Use
HotCellEditorAdvancedComponentwhen you need advanced control over the editor's behavior, positioning, and keyboard shortcuts.
Common Patterns
Pattern 1: Simple Input Editor
A basic text input editor:
import { Component } from "@angular/core";
import { HotCellEditorAdvancedComponent } from "@handsontable/angular-wrapper";
import { FormsModule } from "@angular/forms";
@Component({
selector: "text-editor",
template: `
<input
type="text"
[(ngModel)]="value"
(keydown.enter)="finishEdit.emit()"
(keydown.escape)="cancelEdit.emit()"
style="width: 100%; height: 100%; border: 2px solid blue;"
/>
`,
standalone: true,
imports: [FormsModule],
})
export class TextEditor extends HotCellEditorAdvancedComponent<string> {}
Pattern 2: Dropdown Select Editor
A dropdown editor with predefined options:
import { Component, inject, ChangeDetectorRef } from "@angular/core";
import { HotCellEditorAdvancedComponent, KeyboardShortcutConfig } from "@handsontable/angular-wrapper";
import { CommonModule } from "@angular/common";
@Component({
selector: "select-editor",
template: `
<select [(ngModel)]="value" (change)="finishEdit.emit()" style="width: 100%; height: 100%;">
@for (option of options; track option) {
<option [value]="option">{{ option }}</option>
}
</select>
`,
standalone: true,
imports: [CommonModule, FormsModule],
})
export class SelectEditor extends HotCellEditorAdvancedComponent<string> {
options = ["Option 1", "Option 2", "Option 3"];
override shortcuts?: KeyboardShortcutConfig[] = [
{
keys: [["ArrowDown"]],
callback: (editor) => {
const currentIndex = this.options.indexOf(this.getValue());
const nextIndex = (currentIndex + 1) % this.options.length;
this.setValue(this.options[nextIndex]);
this.cdr.detectChanges();
},
},
];
private readonly cdr = inject(ChangeDetectorRef);
}
Pattern 3: Custom UI with Buttons
An editor with custom button controls:
import { Component } from "@angular/core";
import { HotCellEditorAdvancedComponent } from "@handsontable/angular-wrapper";
import { CommonModule } from "@angular/common";
@Component({
selector: "button-editor",
template: `
<div style="display: flex; gap: 4px; background: #eee; padding: 4px;">
@for (option of options; track option) {
<button [style.backgroundColor]="option === value ? '#90f5e7' : '#fff'" (click)="onSelect(option)">
{{ option }}
</button>
}
</div>
`,
standalone: true,
imports: [CommonModule],
})
export class ButtonEditor extends HotCellEditorAdvancedComponent<string> {
options = ["Yes", "No", "Maybe"];
onSelect(option: string): void {
this.value = option;
this.finishEdit.emit();
}
}
Pattern 4: Keyboard Shortcuts
An editor with comprehensive keyboard navigation:
import { Component, inject, ChangeDetectorRef } from "@angular/core";
import { HotCellEditorAdvancedComponent, KeyboardShortcutConfig } from "@handsontable/angular-wrapper";
import { CommonModule } from "@angular/common";
@Component({
selector: "star-editor",
template: `
<div
style="background: #eee; padding: 5px 8px; cursor: pointer;"
(mouseover)="onMouseOver($event)"
(mousedown)="finishEdit.emit()"
>
@for (star of stars; track $index) {
<span [attr.data-value]="$index + 1" [style.opacity]="$index < getValue() ? '1' : '0.4'"> ⭐ </span>
}
</div>
`,
standalone: true,
imports: [CommonModule],
})
export class StarEditor extends HotCellEditorAdvancedComponent<number> {
readonly stars = Array(5);
override shortcuts?: KeyboardShortcutConfig[] = [
{
keys: [["1"], ["2"], ["3"], ["4"], ["5"]],
callback: (editor, event) => {
this.setValue(parseInt(event.key));
this.cdr.detectChanges();
},
},
{
keys: [["ArrowRight"]],
callback: (editor) => {
if (this.getValue() < 5) {
this.setValue(this.getValue() + 1);
this.cdr.detectChanges();
}
},
},
{
keys: [["ArrowLeft"]],
callback: (editor) => {
if (this.getValue() > 1) {
this.setValue(this.getValue() - 1);
this.cdr.detectChanges();
}
},
},
];
private readonly cdr = inject(ChangeDetectorRef);
onMouseOver(event: MouseEvent): void {
const target = event.target as HTMLElement;
if (target instanceof HTMLSpanElement && target.dataset["value"]) {
this.setValue(parseInt(target.dataset["value"]));
this.cdr.detectChanges();
}
}
}
Pattern 5: Third-Party Library Integration (Angular Material)
Integrating external libraries like Angular Material:
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { HotCellEditorAdvancedComponent } from "@handsontable/angular-wrapper";
import { MatCheckboxModule } from "@angular/material/checkbox";
@Component({
selector: "app-boolean-editor",
standalone: true,
template: `
<div
style="background-color: white; border: 2px solid #1a42e8; display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;"
>
<mat-checkbox [checked]="value" (change)="onCheckboxChange($event.checked)" color="primary"> </mat-checkbox>
</div>
`,
imports: [CommonModule, MatCheckboxModule],
})
export class BooleanEditor extends HotCellEditorAdvancedComponent<boolean> {
onCheckboxChange(checked: boolean): void {
this.value = checked;
this.finishEdit.emit();
}
override setValue(value: boolean): void {
this.value = value ?? false;
}
override getValue(): boolean {
return this.value ?? false;
}
}
Key points for third-party integration:
- Import required external modules in the
importsarray - Override
setValue()andgetValue()for custom value handling - Use
finishEdit.emit()to save changes when user interacts with the external component
Usage in column configuration:
columns: [
{
data: "active",
editor: BooleanEditor,
},
];
TypeScript Support
Both HotCellEditorAdvancedComponent and HotCellRendererAdvancedComponent are fully typed with generics:
// Editor with typed value
export class NumberEditor extends HotCellEditorAdvancedComponent<number> {
// value is automatically typed as number
}
// Renderer with typed value and props
export class CustomRenderer extends HotCellRendererAdvancedComponent<string, { color: string; bold: boolean }> {
// value is typed as string
// getProps() returns { color: string; bold: boolean }
}
// Editor with complex type
interface Product {
id: number;
name: string;
price: number;
}
export class ProductEditor extends HotCellEditorAdvancedComponent<Product> {
// value is typed as Product
// getValue() returns Product
// setValue() expects Product
}
Best Practices
- Use ChangeDetectorRef for manual updates - Inject and call
detectChanges()after programmatic value changes - Use standalone components when possible - Better tree-shaking and module isolation
- Handle keyboard events with shortcuts - Define shortcuts in the
shortcutsarray for consistent behavior - Call
finishEdit.emit()appropriately - When user confirms changes (Enter, blur, button click) - Use
getProps()for renderer configuration - Pass custom properties viarendererProps - Override lifecycle methods as needed -
beforeOpen,afterOpen,afterClose,onFocus - Type your components with generics - Specify value type for type safety:
HotCellEditorAdvancedComponent<YourType>
Column Configuration
Use your custom components in column settings:
import { GridSettings } from "@handsontable/angular-wrapper";
gridSettings: GridSettings = {
columns: [
{
data: "name",
renderer: CustomRenderer,
rendererProps: { color: "blue", bold: true },
},
{
data: "rating",
renderer: StarRenderer,
editor: StarEditor,
},
{
data: "active",
editor: BooleanEditor,
},
],
};
TIP
All the sections below describe how to utilize the features available for the Handsontable factory based editors. This information is also applicable in Angular when you need lower-level control or want to share code between vanilla JavaScript and Angular implementations.
Renderers
Before diving into editors, here's how to create custom renderers:
rendererFactory
A simplified way to create cell renderers.
Signature:
rendererFactory((params) => {
// params.instance - Handsontable instance
// params.td - Table cell element
// params.row - Row index
// params.column - Column index
// params.prop - Property name
// params.value - Cell value
// params.cellProperties - Cell configuration
})
Example:
import { rendererFactory } from 'handsontable/renderers';
const renderer = rendererFactory(({ td, value }) => {
td.style.backgroundColor = value;
td.innerHTML = `<b>${value}</b>`;
});
Just use the parameters you need.
Using editorFactory
The editorFactory helper is the recommended approach for creating custom editors. It handles container creation, positioning, lifecycle management, and shortcuts automatically, allowing you to focus on your editor's unique functionality.
What is editorFactory?
editorFactory is a high-level helper that wraps BaseEditor class construction and handles common patterns automatically. It provides:
- Automatic container creation (
editor.container) - Automatic positioning in
open() - Lifecycle hooks:
beforeOpen,afterOpen,afterInit,afterClose - Built-in shortcut support
- Value/render/config helpers
- Less boilerplate code
- Type-safe custom properties
Basic Usage
Cell Definition Structure
A complete cell definition includes three components:
import { rendererFactory } from 'handsontable/renderers';
import { editorFactory } from 'handsontable/editors';
import { registerCellType } from 'handsontable/cellTypes';
const cellDefinition = {
renderer: rendererFactory(({ td, value }) => {
// Display the cell value
td.innerText = value;
}),
validator: (value, callback) => {
// Validate the value (optional)
callback(!isNaN(parseInt(value)));
},
editor: editorFactory<{input: HTMLInputElement}>({
init(editor) {
editor.input = document.createElement('INPUT') as HTMLInputElement;
// Container is created automatically and `input` is attached automatically
},
getValue(editor) {
return editor.input.value;
},
setValue(editor, value) {
editor.input.value = value;
}
})
};
registerCellType('myCellType', cellDefinition);
// then in Handsontable you can use `"myCellType"` to `type` option to use your cell type.
Signature
editorFactory<CustomProperties, CustomMethods = {}>({
init(editor) { /* Required: Create input element */ },
beforeOpen?(editor, { row, col, prop, td, originalValue, cellProperties }) { /* Per-cell setup */ },
afterOpen?(editor, event?) { /* After editor is positioned and visible */ },
afterInit?(editor) { /* After init and UI attachment, useful for event binding */ },
afterClose?(editor) { /* After editor closes */ },
getValue?(editor) { /* Return current value */ },
setValue?(editor, value) { /* Set value */ },
onFocus?(editor) { /* Custom focus logic */ },
render?(editor) { /* Custom render function */ },
shortcuts?: Array<{ /* Keyboard shortcuts */ }>,
shortcutsGroup?: string, /* Group name for shortcuts */
position?: 'container' | 'portal', /* Positioning strategy */
value?: any, /* Initial value (if CustomProperties has value) */
config?: any, /* Configuration (if CustomProperties has config) */
// ... other optional helpers
})
Lifecycle Methods
Understanding when each method is called:
init(editor)- Called once when the editor is created (singleton pattern)- Create your input element (assign to
editor.input) - Set up event listeners
- Initialize third-party libraries
- ⚠️ Container is created automatically (
editor.container)
- Create your input element (assign to
afterInit(editor)- Called immediately afterinit- Useful for event binding after DOM is ready
- Access to fully initialized editor
beforeOpen(editor, { row, col, prop, td, originalValue, cellProperties })- Called before editor opens- Set editor value from
originalValue - Update settings from
cellProperties - Prepare editor state for the current cell
- ⚠️ This replaces
prepare()when usingeditorFactory
- Set editor value from
afterOpen(editor, event?)- Called after editor is positioned and visible- Open dropdowns, pickers, or other UI elements
- Trigger animations
- Perform actions that require visible editor
- Optional
eventparameter provides the event that triggered the editor opening
afterClose(editor)- Called after editor closes- Cleanup actions
- Reset state if needed
getValue(editor)- Called when saving the value- Return the current editor value
- Optional - defaults to
editor.value
setValue(editor, value)- Called to set the initial value- Update the editor with the cell's current value
- Optional - defaults to setting
editor.value
onFocus(editor)- Custom focus logic- Optional - defaults to focusing first focusable element in container
- In case of special
focusmanagement, add your logic in this hook
render(editor)- Custom render function- Optional - can be used for custom rendering logic
- Receives the editor instance as parameter
value- Initial value property- Optional - can be set directly if your
CustomPropertiestype includes avalueproperty - Automatically typed based on your
CustomPropertiesdefinition
- Optional - can be set directly if your
config- Configuration property- Optional - can be set directly if your
CustomPropertiestype includes aconfigproperty - Automatically typed based on your
CustomPropertiesdefinition
- Optional - can be set directly if your
position- Positioning strategy- Optional - either
'container'(default) or'portal' - Controls how the editor container is positioned in the DOM
- Optional - either
shortcutsGroup- Shortcut group name- Optional - string identifier for grouping keyboard shortcuts
- Useful for organizing shortcuts in complex editors
Custom Properties with TypeScript
Define custom properties for your editor using generics:
type MyEditorProps = {
input: HTMLInputElement; // You create this
container: HTMLDivElement; // Provided automatically by editorFactory
myLibraryInstance: any;
};
const editor = editorFactory<MyEditorProps>({
init(editor) {
// TypeScript knows about editor.input, editor.container, etc.
editor.input = document.createElement('input') as HTMLInputElement;
// editor.container is created automatically
editor.myLibraryInstance = {/***/};
},
getValue(editor) {
return editor.input.value; // Fully typed!
}
});
Common Patterns
Pattern 1: Simple Input Wrapper
For wrapping HTML5 inputs:
editor: editorFactory<{input: HTMLInputElement}>({
init(editor) {
editor.input = document.createElement('input') as HTMLInputElement;
editor.input.type = 'date'; // or 'text', 'color', etc.
// Container is created automatically
},
afterOpen(editor) {
// Open native picker if needed
editor.input.showPicker();
},
getValue(editor) {
return editor.input.value;
},
setValue(editor, value) {
editor.input.value = value;
}
})
Pattern 2: Third-Party Library Integration
For integrating libraries like date pickers, color pickers, etc.:
editor: editorFactory<{input: HTMLInputElement, picker: PickerInstance}>({
init(editor) {
editor.input = document.createElement('input') as HTMLInputElement;
editor.picker = initPicker(editor.input);
// Handle picker events
editor.picker.on('change', () => {
editor.finishEditing();
});
},
afterOpen(editor) {
// Open picker after editor is positioned
editor.picker.open();
}
})
Pattern 3: Preventing Click-Outside Closing
By default, Handsontable will attempt to close a custom editor whenever the user clicks outside the cell or editor container ("click-outside-to-close" behavior). If your editor contains elements like dropdowns, popups, or overlays rendered outside the container, you'll need to prevent this automatic closing when interacting with those UI elements.
When is this needed?
Use this pattern for editors that display dropdowns, popovers, or similar UI elements that aren't direct children of the editor container. Without this, clicking the dropdown will be interpreted as clicking "outside," causing the editor to close unexpectedly.
Using preventCloseElement
Inside your init or afterInit callback, assign an HTMLElement to editor.preventCloseElement (for example, your dropdown or picker DOM node). The factory will attach a mousedown listener to that element that stops propagation, so clicks on it are not treated as "click-outside" and the editor stays open.
Create the element in init or afterInit, assign it to editor.preventCloseElement, and append it to the editor container (or to the document if the dropdown is rendered outside the container).
Example:
const MyEditor = editorFactory({
init(editor) {
editor.input = document.createElement('input');
const dropdownEl = document.createElement('div');
dropdownEl.className = 'my-picker-dropdown';
// Clicks on dropdownEl will not close the editor
editor.preventCloseElement = dropdownEl;
},
getValue(editor) { /* ... */ },
setValue(editor, value) { /* ... */ },
});
Pattern 4: Per-Cell Configuration
Why is this needed?
Handsontable columns can share the same editor, but sometimes you want different cells to behave differently—such as having distinct dropdown options, validation rules, minimum/maximum values, or UI customization. Instead of writing a separate editor for each variation, you can define per-cell properties on the column configuration or in your data. Using the beforeOpen lifecycle hook, you can dynamically read and apply these customizations every time the editor is opened for a specific cell.
Use beforeOpen to read cell-specific settings:
beforeOpen(editor, { originalValue, cellProperties }) {
// Access custom cell properties
const options = cellProperties.customOptions;
// Set initial value
editor.setValue(originalValue);
}
Pattern 5: Keyboard Shortcuts
Why is this needed?
Handsontable is designed to be fully usable with keyboard navigation, allowing users to work efficiently without a mouse. Supporting custom keyboard shortcuts in your editors greatly improves accessibility and power-user productivity. With custom shortcuts, you can let users quickly commit or cancel changes, navigate between UI elements, or trigger special editor behaviors—all from the keyboard.
This is crucial for users who rely on keyboard navigation, require a screen reader, or simply want a faster editing experience. By adding custom shortcuts, your custom editors fully integrate with the keyboard-driven workflow of Handsontable.
Example usage:
editor: editorFactory<{input: HTMLInputElement}>({
init(editor) {
editor.input = document.createElement('div') as HTMLDivElement;
// ... setup
},
shortcuts: [
{
keys: [['ArrowLeft']],
callback: (editor, event) => {
// Custom action for ArrowLeft
return false; // Prevent default
}
},
{
keys: [['1'], ['2'], ['3']],
callback: (editor, event) => {
// Handle number keys
return true; // Don't Prevent default
}
}
]
})
Pattern 6: Overriding Editor Default Behavior
Why is this needed?
Handsontable has default keyboard behaviors that control how editors open, close, and navigate. By default, certain keys trigger specific actions:
- Clicking on another cell (saves changes)
- Pressing Enter (saves changes and moves selection one cell down)
- Pressing Shift+Enter (saves changes and moves selection one cell up)
- Pressing Ctrl/Cmd+Enter or Alt/Option+Enter (adds a new line inside the cell)
- Pressing Escape (aborts changes)
- Pressing Tab (saves changes and moves one cell to the right or to the left, depending on your layout direction)
- Pressing Shift+Tab (saves changes and moves one cell to the left or to the right, depending on your layout direction)
- Pressing Page Up, Page Down (saves changes and moves one screen up/down)
Sometimes you want to override these default behaviors. For example, you might want Tab to cycle through options within your editor instead of moving to the next cell.
Example: Overriding Tab Key Behavior
editor: editorFactory<{input: HTMLDivElement, value: string, config: string[]}>({
config: ['👍', '👎', '🤷♂️'],
init(editor) {
editor.input = editor.hot.rootDocument.createElement("DIV") as HTMLDivElement;
// ... setup
},
shortcuts: [
{
keys: [['Tab']],
callback: (editor, _event) => {
let index = editor.config.indexOf(editor.value);
index = index === editor.config.length - 1 ? 0 : index + 1;
editor.setValue(editor.config[index]);
return false; // Prevents default action
}
}
]
})
How it works:
- Keyboard shortcut
callbackis called for every key press when the editor is active (open) - Return
falseto prevent Handsontable's default behavior for that key - Return
true(or nothing) to allow the default behavior - This gives you full control over keyboard interactions within your editor
Common use cases:
- Making Tab cycle through options instead of moving cells
- Preventing Enter from closing the editor in multi-line inputs
- Adding custom behavior to Escape key
- Overriding navigation keys for custom UI elements
Pattern 7: Using Direct Value and Config Properties
Why is this needed?
Instead of managing state through setValue and getValue, you can define value and config as properties in your CustomProperties type. The factory will automatically handle these properties, making your editor code simpler and more declarative.
Example:
editor: editorFactory<{
input: HTMLInputElement,
value: string,
config: string[]
}>({
config: ['Option 1', 'Option 2', 'Option 3'], // Set directly
init(editor) {
editor.input = document.createElement('INPUT') as HTMLInputElement;
// editor.value and editor.config are automatically available
},
beforeOpen(editor, { originalValue }) {
editor.value = originalValue || editor.config[0]; // Use directly
},
getValue(editor) {
return editor.value; // Access directly
}
})
Pattern 8: Custom Positioning Strategy
Why is this needed?
By default, the editor container is positioned using the 'container' strategy, which places it within the Handsontable container. For editors that need to render outside the normal DOM hierarchy (like portals for dropdowns that need to escape overflow constraints), you can use the 'portal' strategy.
Example:
editor: editorFactory<{input: HTMLInputElement}>({
position: 'portal', // Render outside normal container hierarchy
init(editor) {
editor.input = document.createElement('input') as HTMLInputElement;
}
})
Pattern 9: Organizing Keyboard Shortcuts
Why is this needed?
When you have multiple editors or complex shortcut configurations, organizing shortcuts into groups helps manage conflicts and provides better debugging. The shortcutsGroup option lets you assign a name to your editor's shortcuts.
Example:
editor: editorFactory<{input: HTMLInputElement}>({
shortcutsGroup: 'myCustomEditor',
init(editor) {
editor.input = document.createElement('input') as HTMLInputElement;
},
shortcuts: [
{
keys: [['Enter']],
callback: (editor) => {
// Custom Enter behavior
return false;
}
}
]
})
Usage in Handsontable
Apply your cell definition to columns:
Registering custom cell with registerCellType
import { registerCellType } from 'handsontable/cellTypes';
const cellDefinition = {
renderer: /* ... */,
editor: /* ... */,
customOptions: { /* ... */ }
};
registerCellType('my-type', cellDefinition)
new Handsontable(container, {
data: myData,
columns: [
{ data: 'id', type: 'numeric' },
{
data: 'customField',
type: 'my-type',
}
]
});
Using spread ... operator
new Handsontable(container, {
data: myData,
columns: [
{ data: 'id', type: 'numeric' },
{
data: 'customField',
...cellDefinition, // Spread renderer, validator, editor
// Any custom properties
customOptions: { /* ... */ }
}
]
});
Best Practices with editorFactory
1. Performance
- Create DOM elements in
init(), notafterOpen() - Reuse instances when possible
- Keep renderers simple and fast
2. Positioning
Positioning is handled automatically by editorFactory. You don't need to position the editor manually. The container is automatically positioned over the cell when open() is called.
3. Cleanup
Clean up resources in afterClose() if needed:
afterClose(editor) {
// Release resources if needed
// editor.picker.destroy(); // Example
// Container is hidden automatically
}
4. Validation
Use validators to ensure data integrity:
validator: (value, callback) => {
// Synchronous validation
callback(isValid(value));
}
Examples
👉 Browse All Recipes - Find recipes by use case, difficulty, or technology
We provide complete working examples for common use cases. All examples use the editorFactory helper:
- Color Picker - Integrate a color picker library using
factoryEditor - Feedback Editor - Emoji feedback buttons using
factoryEditor - Flatpickr Date Picker - Advanced date picker with options using
factoryEditor - Pikaday Date Picker - Integrate Pikaday date picker using
factoryEditor - Star Rating - Interactive star rating using
factoryEditor - Multiple Select - Multi-select dropdown using
factoryEditor
Migration from Traditional Approach
If you have existing custom editors, migrating to this approach is optional. The editorFactory method is simply a helper built on top of the existing Editor classes. Your previous custom editors remain fully backward compatible, so you can continue using them as-is or migrate at your convenience.
Before (Traditional):
class CustomEditor extends Handsontable.editors.BaseEditor {
constructor(instance) {
super(instance);
}
init() {
this.wrapper = this.hot.document.root.createElement('div');
this.input = this.hot.document.root.createElement('input');
this.hot.document.appendChild(this.wrapper);
this.wrapper.appendChild(this.input);
// ...
}
getValue() {
return this.input.value;
}
setValue(value) {
this.input.value = value;
}
// ... many more methods
}
After (Using editorFactory):
const editor = editorFactory<{input: HTMLInputElement}>({
init(editor) {
editor.input = document.createElement('input') as HTMLInputElement;
},
getValue(editor) {
return editor.input.value;
}
setValue(editor, value) {
editor.input.value = value;
}
});
Troubleshooting with editorFactory
Editor Not Showing
- Container positioning is handled automatically
- Check that
init()createseditor.inputelement - Verify
afterOpen()is called if you need to trigger UI elements
Value Not Saving
- Verify
getValue()returns the correct value - Check validator is calling
callback(true) - Ensure
setValue()properly updates the editor
Click Outside Closes Immediately
- Use
Event Listenerto stop propagation - See Pattern 3 above
- Use
editor.container(noteditor.wrapper)
Contributing
Have you created a useful custom cell? Consider contributing it as an example!
This approach aims to make Handsontable custom cells as accessible as possible, enabling teams to create custom cells in minutes rather than hours.