Handsontable Cell Type Development
SkillDev toolsUse when creating or modifying a Handsontable cell type that composes an editor, renderer, and validator into a reusable configuration object registered by name
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Handsontable Cell Type Development skill
What this skill tells your AI
The instructions your AI receives, as published by handsontable/handsontable in .claude/skills/handsontable-celltype-dev/SKILL.md and read by ahel’s review.
Structure
Cell types are composition objects, not classes. They bundle an editor, renderer, and validator under a single name:
export const MyCellType = {
CELL_TYPE: 'myType',
editor: MyEditor,
renderer: myRenderer,
validator: myValidator,
// Optional:
valueSetter: customSetter,
valueGetter: customGetter,
valueFormatter: customFormatter,
dataType: 'myType',
};
When a column or cell sets type: 'myType', Handsontable applies all composed components automatically.
File structure
src/cellTypes/{typeName}/
{typeName}.ts # Cell type object
index.ts # Re-exports
Registry: src/cellTypes/registry.ts.
Registration
import { registerCellType } from '../../cellTypes/registry';
registerCellType(MyCellType);
Also export from src/cellTypes/index.ts so the type is available in the full bundle.
Integration with metaSchema
New cell types must be added to src/dataMap/metaManager/metaSchema.ts so Handsontable recognizes the type name in configuration. Add the type string to the type option's accepted values.
Key rules
- Think of cell types as pre-configured bundles. They exist for convenience - users set one
typeinstead of specifyingeditor,renderer, andvalidatorseparately. - All components are optional. A cell type can omit
validatorif no validation is needed, or omiteditorfor read-only display types. - Individual overrides win. If a user sets both
type: 'myType'andrenderer: customRenderer, the explicitrenderertakes precedence over the one from the cell type. valueSetteris the ONLY place a type may normalize an incoming value — never the editor alone, and never a plugin. A value reaches a cell by many routes, and the editor is only one of them: a paste,setDataAtCell(),populateFromArray(), autofill and undo all bypass it.valueSetterruns on every one of those, so a type whose stored shape differs from what the user writes (a key/valuesource, a complex-format type) must resolve it there. Two rules come with that, both learned from DEV-57, where the autocomplete editor resolved a typed label againstsourcewhile nothing else did — a pasted label was stored as a bare string among key/value objects, and astrictdropdownthen marked the cell invalid:- Share the rule with the editor, do not copy it. The single implementation is
findChoiceByDisplayedValue()(utils/cellSource.ts), called by bothautocompleteEditor#getValue()and the autocompletevalueSetter. Two copies of one matching rule is exactly what let those paths drift. - Anything exported from
src/helpers/**is public API forever, types included.index.tsspreads those modules ontoHandsontable.helperandbase.tstypes the namespace astypeof import('./helpers/object'), so a new export there is a permanent maintenance commitment and a narrowed signature is a break. That is whyutils/cellSource.tsholds the whole key/value rule — includingisKeyValueEntry(), the narrowing form of the publicisKeyValueObject(). It delegates to the public function rather than repeating the shape test, so the two cannot disagree, andhelpers/object.tskeeps a zero diff. Reach forsrc/utils/for anything a cell type needs. valueSettertakes five arguments:(value, visualRow, visualCol, cellMeta, source).utils/valueAccessors.tspasses all five, socellMeta.source,cellMeta.allowHtmland the change source need no plumbing. Type the meta parameter as aPick<CellProperties, …>of the fields you read, so a unit test need not build a whole meta object; read anything else throughthis.getCellMetaTransient, neverthis.getCellMeta(see the coreAGENTS.md). Thesourceparameter is declared optional on the public type on purpose — a required fifth parameter would raise the option's minimum call arity and break a consumer that reads the option back out and calls it with four (.ai/BREAKING-CHANGES.md).- Never write a delegating setter by hand — re-export.
dropdownType/accessors/valueSetter.tsused to be a hand-written delegate, and it droppedcellMeta, which left the strict column — the one where the bug is visible — unfixed while the non-strict one worked. It is nowexport { valueSetter } from '../../autocompleteType/accessors';: a re-export has no argument list to keep in sync, so that class of mistake is gone rather than documented. A unit test pins the identity (DropdownCellType.valueSetterisAutocompleteCellType.valueSetter). - Skip every transformation on
UndoRedo.*.utils/valueAccessors.tsstates the invariant — undo and redo restore what the cell held before, verbatim — and honors it foremptyValue. The autocomplete setter did not: it wrapped a restored plain label as{ key: <label>, value: <label> }whenever the cell happened to hold an entry, so undoing a column loaded with plain labels produced a fabricated pair astrictcolumn then rejected. ReturnnewValueuntouched whensourcestarts with'UndoRedo.'. - Guard an empty write.
isEmpty(newValue)must skip any resolution, or asourceentry carrying an empty label stands in for "no value" andallowEmptystops meaning what it says. - Gate the expensive part on a cheap shape check. The setter runs once per changed cell, so a paste of thousands of rows multiplies whatever it does.
hasKeyValueChoices()reads only the entries' shape — no string work — so a column whosesourceholds plain strings never pays for a label scan it could not use. Do not memoize the scan itself: asourcearray can be mutated in place by the host application, and a stale displayed-text map would resolve a label to an option no longer offered. - The gate bounds who pays, not how much. A column that does hold key/value entries still runs
findChoiceByDisplayedValue()per changed cell, and that is a linear scan which callsstringify()andstripTags()on every choice it walks —stripTags()reads the label character by character. Cost is thereforechanged cells × source size. At realistic dropdown sizes (10–100 options) a 10k-row paste stays in single-digit milliseconds, but a source in the hundreds-to-thousands turns the same paste into roughly a second of scanning. That is the accepted price of never serving a stale option; if a source that large ever needs to be fast, the fix is a map invalidated by identity, not a plain cache.
- Share the rule with the editor, do not copy it. The single implementation is
Reference implementations
src/cellTypes/numericType/numericType.ts- Composes numeric editor, renderer, and validator.src/cellTypes/textType/textType.ts- Simplest type, good starting template.src/cellTypes/dateType/dateType.ts- Date handling with format options.src/cellTypes/checkboxType/checkboxType.ts- Boolean toggle pattern.
Common mistakes
- Forgetting to register the cell type in
src/cellTypes/registry.ts. - Not adding the type to
metaSchema.ts, causing Handsontable to ignore the type name. - Duplicating editor/renderer/validator logic instead of importing existing components.
- Not exporting from
src/cellTypes/index.tsfor the full bundle.
Signals
- GitHub stars
- 22k
- Forks
- 3k
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
handsontable-celltype-dev- Source
- github.com/handsontable/handsontable