Use when creating a new Handsontable plugin, modifying an existing plugin's behavior, adding hooks or options to a plugin, or working with the plugin lifecycle (enablePlugin, disablePlugin, updatePlugin). Covers the full plugin contract, conflict registration, settings validation, and IndexMapper integration.
src/plugins/{pluginName}/
├── index.js # Re-exports PLUGIN_KEY, PLUGIN_PRIORITY, ClassName
├── {pluginName}.js # Main class extending BasePlugin
├── __tests__/ # Tests (*.spec.js for E2E, *.unit.js for unit)
└── {submodules}/ # Additional files (UI classes, strategies, etc.)
| Property | Purpose | Example |
|---|---|---|
PLUGIN_KEY | Unique camelCase identifier | 'pagination' |
PLUGIN_PRIORITY | Execution order (higher = later) | 900 |
SETTING_KEYS | Options triggering updatePlugin | ['pagination'], true (always), (never) |
falsePLUGIN_DEPS | Required plugins/types | ['plugin:AutoRowSize'] |
DEFAULT_SETTINGS | Defaults for this.getSetting() | { pageSize: 10 } |
SETTINGS_VALIDATORS | Validate settings (object map or single fn) | { pageSize: v => v > 0 } |
isEnabled() // return !!this.hot.getSettings()[PLUGIN_KEY]
enablePlugin() // init state, create IndexMaps, register hooks. Call super.enablePlugin() AT THE END.
updatePlugin() // this.disablePlugin(); this.enablePlugin(); super.updatePlugin();
disablePlugin() // Call super.disablePlugin() FIRST (clears hooks/EventManager). Then clean up.
destroy() // Null out all fields. Call super.destroy() AT THE END.
Private fields -- Use # prefix for all internal state. No @private JSDoc.
Hook callbacks -- Arrow function class fields so removeLocalHook works:
#onIndexCacheUpdate = () => {
if (!this.#internalCall && this.hot?.view) {
this.#recompute();
}
};
Hook registration -- this.addHook() auto-cleans on disablePlugin(). this.hot.addHook() does NOT.
Register new hook names at module level:
import Hooks from '../../core/hooks';
Hooks.getSingleton().register('beforeMyAction');
Settings -- Read via this.getSetting('key') (supports dot notation). Defaults come from DEFAULT_SETTINGS.
Conflict registration -- At module level, before the class:
import { registerConflict } from '../base/conflictRegistry';
registerConflict(PLUGIN_KEY, ['nestedRows', 'mergeCells']);
Check in enablePlugin() with this.isHardConflictBlocked().
IndexMapper -- Create maps in enablePlugin(), unregister in disablePlugin():
this.#map = this.hot.rowIndexMapper.createAndRegisterIndexMap(this.pluginName, 'hiding', false);
// 'hiding' = HidingMap (not rendered, stays in DataMap)
// 'trimming' = TrimmingMap (removed from DataMap entirely)
UI separation -- Extract UI into its own class with dependency injection (no direct hot reference).
Strategy pattern -- Use for swappable logic (e.g., autoPageSize vs fixedPageSize).
Batch rendering -- When making multiple data/render changes, wrap them to avoid redundant render cycles:
this.hot.batch(() => {
// multiple operations here -- only one render at the end
});
// Or for render-only batching:
this.hot.suspendRender();
// ... operations ...
this.hot.resumeRender();
hot.getPlugin('{Name}').getPlugin('notification') when notification is enabled (error toasts). Fetch failures include a primary Refetch action and duration: 0 so the user can retry fetchData() from the toast. It does not use Dialog for that path. Dialog is still used elsewhere (for example Loading plugin, ExportFile overlay). Prefer hooks (afterDataProviderFetchError, afterRowsMutationError) for fully custom error UI when Notification is off.index.js: export { PLUGIN_KEY, PLUGIN_PRIORITY, ClassName } from './pluginName';src/plugins/index.js.src/dataMap/metaManager/metaSchema.js.types/.If your plugin provides UI elements (buttons, inputs, navigation bars), you must integrate with the focus manager (src/focusManager/).
#registerFocusScope / #unregisterFocusScope).colspan/rowspan from hot.getCellMeta(row, col) (set by MergeCells via afterGetCellMeta), not from DOM element attributes. The meta is authoritative and always available regardless of viewport state.__tests__/*.spec.js): all it() callbacks must be async.__tests__/*.unit.js): test strategies and helpers in isolation.updateSettings(), enablePlugin()/disablePlugin() toggling.Gold standard: src/plugins/pagination/pagination.js. Base class: src/plugins/base/base.js.
See .ai/ARCHITECTURE.md and .ai/CONVENTIONS.md for deeper context.