Angular Data GridSimplified Custom Cell Definitions

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:

  1. Steep Learning Curve: Requires understanding of EditorManager, BaseEditor lifecycle, and complex inheritance patterns
  2. Boilerplate Code: Lots of repetitive code for simple customizations
  3. Error-Prone: Easy to miss critical lifecycle methods or forget to call super methods
  4. Poor Developer Experience: Not optimized for modern workflows (AI assistance, quick prototyping)
  5. Functional programming patterns are increasingly popular - example React moved from class components to hooks specifically to avoid this confusion

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 renderers
  • HotCellEditorAdvancedComponent - 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 rendererProps or config

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 instance
  • td: HTMLTableCellElement - The cell's TD element
  • row: number - Row index
  • col: number - Column index
  • prop: string - Property name
  • cellProperties: 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 HotCellRendererComponent for renderers based on the traditional baseRenderer.
  • Use HotCellRendererAdvancedComponent when you want to align with the newer rendererFactory pattern 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 edited
  • column: number - Column index of the cell being edited
  • prop: string | number - Property name of the cell being edited
  • originalValue: T - Original value of the cell before editing
  • cellProperties: CellProperties - Cell configuration

Output Events

  • finishEdit: EventEmitter<void> - Emit to save changes and close the editor
  • cancelEdit: EventEmitter<void> - Emit to cancel changes and revert to original value

Lifecycle Methods

Override these methods to customize editor behavior:

  1. onFocus(editor?: ExtendedEditor<T>): void

    • Called when the editor receives focus
    • Use for custom focus management
  2. afterOpen(editor: ExtendedEditor<T>, event?: Event): void

    • Called after the editor is opened and positioned
    • Use to open dropdowns, trigger animations, or focus elements
  3. afterClose(editor: ExtendedEditor<T>): void

    • Called when the editor closes
    • Use for cleanup actions
  4. afterInit(editor: ExtendedEditor<T>): void

    • Called after the editor is initialized
    • Use for one-time setup
  5. 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 configuration
  • shortcutsGroup?: string - Group name for shortcuts
  • config?: 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 HotCellEditorComponent for simple, standard editors.
  • Use HotCellEditorAdvancedComponent when 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 imports array
  • Override setValue() and getValue() 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

  1. Use ChangeDetectorRef for manual updates - Inject and call detectChanges() after programmatic value changes
  2. Use standalone components when possible - Better tree-shaking and module isolation
  3. Handle keyboard events with shortcuts - Define shortcuts in the shortcuts array for consistent behavior
  4. Call finishEdit.emit() appropriately - When user confirms changes (Enter, blur, button click)
  5. Use getProps() for renderer configuration - Pass custom properties via rendererProps
  6. Override lifecycle methods as needed - beforeOpen, afterOpen, afterClose, onFocus
  7. 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:

  1. 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)
  2. afterInit(editor) - Called immediately after init

    • Useful for event binding after DOM is ready
    • Access to fully initialized editor
  3. 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 using editorFactory
  4. 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 event parameter provides the event that triggered the editor opening
  5. afterClose(editor) - Called after editor closes

    • Cleanup actions
    • Reset state if needed
  6. getValue(editor) - Called when saving the value

    • Return the current editor value
    • Optional - defaults to editor.value
  7. setValue(editor, value) - Called to set the initial value

    • Update the editor with the cell's current value
    • Optional - defaults to setting editor.value
  8. onFocus(editor) - Custom focus logic

    • Optional - defaults to focusing first focusable element in container
    • In case of special focus management, add your logic in this hook
  9. render(editor) - Custom render function

    • Optional - can be used for custom rendering logic
    • Receives the editor instance as parameter
  10. value - Initial value property

    • Optional - can be set directly if your CustomProperties type includes a value property
    • Automatically typed based on your CustomProperties definition
  11. config - Configuration property

    • Optional - can be set directly if your CustomProperties type includes a config property
    • Automatically typed based on your CustomProperties definition
  12. position - Positioning strategy

    • Optional - either 'container' (default) or 'portal'
    • Controls how the editor container is positioned in the DOM
  13. 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 callback is called for every key press when the editor is active (open)
  • Return false to 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(), not afterOpen()
  • 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:

  1. Color Picker - Integrate a color picker library using factoryEditor
  2. Feedback Editor - Emoji feedback buttons using factoryEditor
  3. Flatpickr Date Picker - Advanced date picker with options using factoryEditor
  4. Pikaday Date Picker - Integrate Pikaday date picker using factoryEditor
  5. Star Rating - Interactive star rating using factoryEditor
  6. 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() creates editor.input element
  • 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 Listener to stop propagation
  • See Pattern 3 above
  • Use editor.container (not editor.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.