Moment.js-based date
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { getRenderer } from 'handsontable/renderers';import { editorFactory } from 'handsontable/editors';import { registerCellType } from 'handsontable/cellTypes';import moment from 'moment';import Pikaday from '@handsontable/pikaday';
// Register all Handsontable's modules.registerAllModules();
/* start:skip-in-preview */const data = [ { id: 640329, itemName: 'Lunar Core', itemNo: 'XJ-12', leadEngineer: 'Ellen Ripley', cost: 350000, inStock: true, category: 'Lander', itemQuality: 87, origin: '🇺🇸 USA', quantity: 2, valueStock: 700000, repairable: false, supplierName: 'TechNova', restockDate: '2025-08-01', operationalStatus: 'Awaiting Parts', }, { id: 863104, itemName: 'Zero Thrusters', itemNo: 'QL-54', leadEngineer: 'Sam Bell', cost: 450000, inStock: false, category: 'Propulsion', itemQuality: 0, origin: '🇩🇪 Germany', quantity: 0, valueStock: 0, repairable: true, supplierName: 'PropelMax', restockDate: '2025-09-15', operationalStatus: 'In Maintenance', }, { id: 395603, itemName: 'EVA Suits', itemNo: 'PM-67', leadEngineer: 'Alex Rogan', cost: 150000, inStock: true, category: 'Equipment', itemQuality: 79, origin: '🇮🇹 Italy', quantity: 50, valueStock: 7500000, repairable: true, supplierName: 'SuitCraft', restockDate: '2025-10-05', operationalStatus: 'Ready for Testing', }, { id: 679083, itemName: 'Solar Panels', itemNo: 'BW-09', leadEngineer: 'Dave Bowman', cost: 75000, inStock: true, category: 'Energy', itemQuality: 95, origin: '🇺🇸 USA', quantity: 10, valueStock: 750000, repairable: false, supplierName: 'SolarStream', restockDate: '2025-11-10', operationalStatus: 'Operational', }, { id: 912663, itemName: 'Comm Array', itemNo: 'ZR-56', leadEngineer: 'Louise Banks', cost: 125000, inStock: false, category: 'Communication', itemQuality: 0, origin: '🇯🇵 Japan', quantity: 0, valueStock: 0, repairable: true, supplierName: 'CommTech', restockDate: '2025-12-20', operationalStatus: 'Decommissioned', }, { id: 315806, itemName: 'Habitat Dome', itemNo: 'UJ-23', leadEngineer: 'Dr. Ryan Stone', cost: 1000000, inStock: true, category: 'Shelter', itemQuality: 93, origin: '🇨🇦 Canada', quantity: 3, valueStock: 3000000, repairable: false, supplierName: 'DomeInnovate', restockDate: '2026-01-25', operationalStatus: 'Operational', },];
/* end:skip-in-preview */// Get the DOM element with the ID 'example1' where the Handsontable will be renderedconst container = document.querySelector('#example1');const correctFormat = (value, dateFormat) => { const dateFromDate = moment(value); const dateFromMoment = moment(value, dateFormat); const isAlphanumeric = value.search(/[A-Za-z]/g) > -1; let date;
if ( (dateFromDate.isValid() && dateFromDate.format('x') === dateFromMoment.format('x')) || !dateFromMoment.isValid() || isAlphanumeric ) { date = dateFromDate; } else { date = dateFromMoment; }
return date.format(dateFormat);};
const cellDateTypeDefinition = { renderer: getRenderer('autocomplete'), validator(value, callback) { let valid = true;
if (value === null || value === undefined) { value = ''; }
let isValidFormat = moment(value, this.dateFormat, true).isValid(); let isValidDate = moment(new Date(value)).isValid() || isValidFormat;
if (this.allowEmpty && value === '') { isValidDate = true; isValidFormat = true; }
if (!isValidDate) { valid = false; }
if (!isValidDate && isValidFormat) { valid = true; }
if (isValidDate && !isValidFormat) { if (this.correctFormat === true) { const correctedValue = correctFormat(value, this.dateFormat);
this.instance.setDataAtCell(this.visualRow, this.visualCol, correctedValue, 'dateValidator'); valid = true; } else { valid = false; } }
callback(valid); }, editor: editorFactory({ position: 'portal', shortcuts: [ { keys: [['ArrowLeft']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('subtract', 1); _event.preventDefault(); }, }, { keys: [['ArrowRight']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('add', 1); _event.preventDefault(); }, }, { keys: [['ArrowUp']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('subtract', 7); _event.preventDefault(); }, }, { keys: [['ArrowDown']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('add', 7); _event.preventDefault(); }, }, ], init(editor) { editor.parentDestroyed = false; // create the input element on init. This is a text input that color picker will be attached to. editor.input = editor.hot.rootDocument.createElement('input'); editor.datePicker = editor.container; // Prevent recognizing clicking on datepicker as clicking outside of table. editor.hot.rootDocument.addEventListener('mousedown', (event) => { if (event.target && event.target.classList.contains('pika-day')) { editor.hideDatepicker(editor); } }); }, getDatePickerConfig(editor) { const htInput = editor.input; const options = {};
if (editor.cellProperties && editor.cellProperties.datePickerConfig) { Object.assign(options, editor.cellProperties.datePickerConfig); }
const origOnSelect = options.onSelect; const origOnClose = options.onClose;
options.field = htInput; options.trigger = htInput; options.container = editor.datePicker; options.bound = false; options.keyboardInput = false; options.format = options.format ?? editor.getDateFormat(editor); options.reposition = options.reposition || false; // Set the RTL to `false`. Due to the https://github.com/Pikaday/Pikaday/issues/647 bug, the layout direction // of the date picker is controlled by juggling the "dir" attribute of the root date picker element. options.isRTL = false; options.onSelect = function (date) { let dateStr;
if (!isNaN(date.getTime())) { dateStr = moment(date).format(editor.getDateFormat(editor)); }
editor.setValue(dateStr);
if (origOnSelect) { origOnSelect.call(editor.pikaday, date); }
if (Handsontable.helper.isMobileBrowser()) { editor.hideDatepicker(editor); } }; options.onClose = () => { if (!editor.parentDestroyed) { editor.finishEditing(false); }
if (origOnClose) { origOnClose(); } };
return options; }, hideDatepicker(editor) { editor.pikaday.hide(); }, showDatepicker(editor, event) { const dateFormat = editor.getDateFormat(editor); // TODO: view is not exported in the handsontable library d.ts, so we need to use @ts-ignore // @ts-ignore const isMouseDown = editor.hot.view.isMouseDown(); const isMeta = event && 'keyCode' in event ? Handsontable.helper.isFunctionKey(event.keyCode) : false; let dateStr;
editor.datePicker.style.display = 'block'; editor.pikaday = new Pikaday(editor.getDatePickerConfig(editor));
// TODO: useMoment is not exported in the pikaday library d.ts, so we need to use @ts-ignore // @ts-ignore if (typeof editor.pikaday.useMoment === 'function') { // @ts-ignore editor.pikaday.useMoment(moment); }
// TODO: _onInputFocus is not exported in the pikaday library d.ts, so we need to use @ts-ignore // @ts-ignore editor.pikaday._onInputFocus = function () {};
if (editor.originalValue) { dateStr = editor.originalValue;
if (moment(dateStr, dateFormat, true).isValid()) { editor.pikaday.setMoment(moment(dateStr, dateFormat), true); }
// workaround for date/time cells - pikaday resets the cell value to 12:00 AM by default, this will overwrite the value. if (editor.getValue() !== editor.originalValue) { editor.setValue(editor.originalValue); }
if (!isMeta && !isMouseDown) { editor.setValue(''); } } else if (editor.cellProperties.defaultDate) { dateStr = editor.cellProperties.defaultDate;
if (moment(dateStr, dateFormat, true).isValid()) { editor.pikaday.setMoment(moment(dateStr, dateFormat), true); }
if (!isMeta && !isMouseDown) { editor.setValue(''); } } else { // if a default date is not defined, set a soft-default-date: display the current day and month in the // datepicker, but don't fill the editor input editor.pikaday.gotoToday(); } }, afterClose(editor) { if (editor.pikaday.destroy) { editor.pikaday.destroy(); } }, afterOpen(editor, event) { const cellRect = editor.TD.getBoundingClientRect();
editor.input.style.width = `${cellRect.width}px`; editor.input.style.height = `${cellRect.height}px`; editor.showDatepicker(editor, event); }, getValue(editor) { return editor.input.value; }, setValue(editor, value) { editor.input.value = value; }, getDateFormat(editor) { return editor.cellProperties.dateFormat ?? 'DD/MM/YYYY'; }, }),};
registerCellType('moment-date', cellDateTypeDefinition);
// Define configuration options for the Handsontableconst hotOptions = { data, colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Restock Date', 'Cost'], autoRowSize: true, rowHeaders: true, height: 'auto', width: '100%', autoWrapRow: true, headerClassName: 'htLeft', columns: [ { data: 'itemName', type: 'text', width: 130 }, { data: 'category', type: 'text', width: 120 }, { data: 'leadEngineer', type: 'text', width: 150 }, { data: 'restockDate', type: 'moment-date', width: 150, dateFormat: 'YYYY-MM-DD', correctFormat: true, datePickerConfig: { firstDay: 0, showWeekNumber: true, disableDayFn(date) { return date.getDay() === 0 || date.getDay() === 6; }, }, }, { data: 'cost', type: 'numeric', width: 120, className: 'htRight', numericFormat: { pattern: '$0,0.00', culture: 'en-US', }, }, ], licenseKey: 'non-commercial-and-evaluation',};
// Initialize the Handsontable instance with the specified configuration options// eslint-disable-next-line no-unused-varsconst hot = new Handsontable(container, hotOptions);import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { getRenderer } from 'handsontable/renderers';import { editorFactory } from 'handsontable/editors';import { registerCellType } from 'handsontable/cellTypes';import moment from 'moment';import Pikaday from '@handsontable/pikaday';
// Register all Handsontable's modules.registerAllModules();
/* start:skip-in-preview */const data = [ { id: 640329, itemName: 'Lunar Core', itemNo: 'XJ-12', leadEngineer: 'Ellen Ripley', cost: 350000, inStock: true, category: 'Lander', itemQuality: 87, origin: '🇺🇸 USA', quantity: 2, valueStock: 700000, repairable: false, supplierName: 'TechNova', restockDate: '2025-08-01', operationalStatus: 'Awaiting Parts', }, { id: 863104, itemName: 'Zero Thrusters', itemNo: 'QL-54', leadEngineer: 'Sam Bell', cost: 450000, inStock: false, category: 'Propulsion', itemQuality: 0, origin: '🇩🇪 Germany', quantity: 0, valueStock: 0, repairable: true, supplierName: 'PropelMax', restockDate: '2025-09-15', operationalStatus: 'In Maintenance', }, { id: 395603, itemName: 'EVA Suits', itemNo: 'PM-67', leadEngineer: 'Alex Rogan', cost: 150000, inStock: true, category: 'Equipment', itemQuality: 79, origin: '🇮🇹 Italy', quantity: 50, valueStock: 7500000, repairable: true, supplierName: 'SuitCraft', restockDate: '2025-10-05', operationalStatus: 'Ready for Testing', }, { id: 679083, itemName: 'Solar Panels', itemNo: 'BW-09', leadEngineer: 'Dave Bowman', cost: 75000, inStock: true, category: 'Energy', itemQuality: 95, origin: '🇺🇸 USA', quantity: 10, valueStock: 750000, repairable: false, supplierName: 'SolarStream', restockDate: '2025-11-10', operationalStatus: 'Operational', }, { id: 912663, itemName: 'Comm Array', itemNo: 'ZR-56', leadEngineer: 'Louise Banks', cost: 125000, inStock: false, category: 'Communication', itemQuality: 0, origin: '🇯🇵 Japan', quantity: 0, valueStock: 0, repairable: true, supplierName: 'CommTech', restockDate: '2025-12-20', operationalStatus: 'Decommissioned', }, { id: 315806, itemName: 'Habitat Dome', itemNo: 'UJ-23', leadEngineer: 'Dr. Ryan Stone', cost: 1000000, inStock: true, category: 'Shelter', itemQuality: 93, origin: '🇨🇦 Canada', quantity: 3, valueStock: 3000000, repairable: false, supplierName: 'DomeInnovate', restockDate: '2026-01-25', operationalStatus: 'Operational', },];/* end:skip-in-preview */// Get the DOM element with the ID 'example1' where the Handsontable will be renderedconst container = document.querySelector('#example1')!;
const correctFormat = (value: string, dateFormat: string): string => { const dateFromDate = moment(value); const dateFromMoment = moment(value, dateFormat); const isAlphanumeric = value.search(/[A-Za-z]/g) > -1; let date;
if ((dateFromDate.isValid() && dateFromDate.format('x') === dateFromMoment.format('x')) || !dateFromMoment.isValid() || isAlphanumeric) { date = dateFromDate;
} else { date = dateFromMoment; }
return date.format(dateFormat);}
const cellDateTypeDefinition = { renderer: getRenderer('autocomplete'), validator: function(value, callback) { let valid = true;
if (value === null || value === undefined) { value = ''; }
let isValidFormat = moment(value, this.dateFormat, true).isValid(); let isValidDate = moment(new Date(value)).isValid() || isValidFormat;
if (this.allowEmpty && value === '') { isValidDate = true; isValidFormat = true; } if (!isValidDate) { valid = false; } if (!isValidDate && isValidFormat) { valid = true; }
if (isValidDate && !isValidFormat) { if (this.correctFormat === true) { const correctedValue = correctFormat(value, this.dateFormat);
this.instance.setDataAtCell(this.visualRow, this.visualCol, correctedValue, 'dateValidator'); valid = true; } else { valid = false; } }
callback(valid); }, editor: editorFactory({ position: 'portal', shortcuts: [ { keys: [['ArrowLeft']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('subtract', 1); _event.preventDefault(); }, }, { keys: [['ArrowRight']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('add', 1); _event.preventDefault(); }, }, { keys: [['ArrowUp']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('subtract', 7); _event.preventDefault(); }, }, { keys: [['ArrowDown']], callback: (editor, _event) => { // @ts-ignore editor.pikaday.adjustDate('add', 7); _event.preventDefault(); }, }, ], init(editor) { editor.parentDestroyed = false; // create the input element on init. This is a text input that color picker will be attached to. editor.input = editor.hot.rootDocument.createElement('input'); editor.datePicker = editor.container; // Prevent recognizing clicking on datepicker as clicking outside of table. editor.hot.rootDocument.addEventListener('mousedown', (event) => { if (event.target && event.target.classList.contains('pika-day')) { editor.hideDatepicker(editor); } }); }, getDatePickerConfig(editor) { const htInput = editor.input; const options = {};
if (editor.cellProperties && editor.cellProperties.datePickerConfig) { Object.assign(options, editor.cellProperties.datePickerConfig); }
const origOnSelect = options.onSelect; const origOnClose = options.onClose;
options.field = htInput; options.trigger = htInput; options.container = editor.datePicker; options.bound = false; options.keyboardInput = false; options.format = options.format ?? editor.getDateFormat(editor); options.reposition = options.reposition || false; // Set the RTL to `false`. Due to the https://github.com/Pikaday/Pikaday/issues/647 bug, the layout direction // of the date picker is controlled by juggling the "dir" attribute of the root date picker element. options.isRTL = false; options.onSelect = function (date) { let dateStr;
if (!isNaN(date.getTime())) { dateStr = moment(date).format(editor.getDateFormat(editor)); }
editor.setValue(dateStr);
if (origOnSelect) { origOnSelect.call(editor.pikaday, date); }
if (Handsontable.helper.isMobileBrowser()) { editor.hideDatepicker(editor); } }; options.onClose = () => { if (!editor.parentDestroyed) { editor.finishEditing(false); }
if (origOnClose) { origOnClose(); } };
return options; }, hideDatepicker(editor) { editor.pikaday.hide(); }, showDatepicker(editor, event) { const dateFormat = editor.getDateFormat(editor); // TODO: view is not exported in the handsontable library d.ts, so we need to use @ts-ignore // @ts-ignore const isMouseDown = editor.hot.view.isMouseDown(); const isMeta = event && 'keyCode' in event ? Handsontable.helper.isFunctionKey(event.keyCode) : false;
let dateStr;
editor.datePicker.style.display = 'block'; editor.pikaday = new Pikaday(editor.getDatePickerConfig(editor));
// TODO: useMoment is not exported in the pikaday library d.ts, so we need to use @ts-ignore // @ts-ignore if (typeof editor.pikaday.useMoment === 'function') { // @ts-ignore editor.pikaday.useMoment(moment); }
// TODO: _onInputFocus is not exported in the pikaday library d.ts, so we need to use @ts-ignore // @ts-ignore editor.pikaday._onInputFocus = function () {};
if (editor.originalValue) { dateStr = editor.originalValue;
if (moment(dateStr, dateFormat, true).isValid()) { editor.pikaday.setMoment(moment(dateStr, dateFormat), true); }
// workaround for date/time cells - pikaday resets the cell value to 12:00 AM by default, this will overwrite the value. if (editor.getValue() !== editor.originalValue) { editor.setValue(editor.originalValue); }
if (!isMeta && !isMouseDown) { editor.setValue(''); } } else if (editor.cellProperties.defaultDate) { dateStr = editor.cellProperties.defaultDate;
if (moment(dateStr, dateFormat, true).isValid()) { editor.pikaday.setMoment(moment(dateStr, dateFormat), true); }
if (!isMeta && !isMouseDown) { editor.setValue(''); } } else { // if a default date is not defined, set a soft-default-date: display the current day and month in the // datepicker, but don't fill the editor input editor.pikaday.gotoToday(); } }, afterClose(editor) { if (editor.pikaday.destroy) { editor.pikaday.destroy(); } }, afterOpen(editor, event) { const cellRect = editor.TD.getBoundingClientRect();
editor.input.style.width = `${cellRect.width}px`; editor.input.style.height = `${cellRect.height}px`; editor.showDatepicker(editor, event); }, getValue(editor) { return editor.input.value; }, setValue(editor, value) { editor.input.value = value; }, getDateFormat(editor) { return editor.cellProperties.dateFormat ?? 'DD/MM/YYYY'; }, }),};
registerCellType('moment-date', cellDateTypeDefinition);
// Define configuration options for the Handsontableconst hotOptions: Handsontable.GridSettings = { data, colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Restock Date', 'Cost'], autoRowSize: true, rowHeaders: true, height: 'auto', width: '100%', autoWrapRow: true, headerClassName: 'htLeft', columns: [ { data: 'itemName', type: 'text', width: 130 }, { data: 'category', type: 'text', width: 120 }, { data: 'leadEngineer', type: 'text', width: 150 }, { data: 'restockDate', type: 'moment-date', width: 150, dateFormat: 'YYYY-MM-DD', correctFormat: true, datePickerConfig: { firstDay: 0, showWeekNumber: true, disableDayFn(date: Date) { return date.getDay() === 0 || date.getDay() === 6; }, }, }, { data: 'cost', type: 'numeric', width: 120, className: 'htRight', numericFormat: { pattern: '$0,0.00', culture: 'en-US', }, }, ], licenseKey: 'non-commercial-and-evaluation',};
// Initialize the Handsontable instance with the specified configuration options// eslint-disable-next-line no-unused-varsconst hot = new Handsontable(container, hotOptions);.ht_editor_visible > input { width: 100%; height: 100%; box-sizing: border-box !important; border: none; border-radius: 0; outline: none; margin-top: -1px; margin-left: -1px; box-shadow: inset 0 0 0 var(--ht-cell-editor-border-width, 2px) var(--ht-cell-editor-border-color, #1a42e8), 0 0 var(--ht-cell-editor-shadow-blur-radius, 0) 0 var(--ht-cell-editor-shadow-color, transparent) !important; background-color: var(--ht-cell-editor-background-color, #ffffff) !important; padding: var(--ht-cell-vertical-padding, 4px) var(--ht-cell-horizontal-padding, 8px) !important; border: none !important; font-family: inherit; font-size: var(--ht-font-size); line-height: var(--ht-line-height);}.ht_editor_visible > input:focus-visible { border: none !important; outline: none !important;}Overview
This guide shows how to create a custom date cell type using the Moment.js library. Users can format dates using the Moment.js API.
Difficulty: Beginner
Time: ~25 minutes
Libraries: moment, @handsontable/pikaday
What You’ll Build
A cell that:
- Displays dates with a dropdown arrow indicator
- Opens a Pikaday calendar picker when edited
- Validates and corrects date formats using Moment.js
- Supports custom
dateFormatoptions per column - Disables weekend selection via
datePickerConfig
Prerequisites
npm install moment @handsontable/pikadayImport Dependencies
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { getRenderer } from 'handsontable/renderers';import { editorFactory } from 'handsontable/editors';import { registerCellType } from 'handsontable/cellTypes';import moment from 'moment';import Pikaday from '@handsontable/pikaday';registerAllModules();Why this matters:
momenthandles date parsing, validation, and formattingPikadayprovides the calendar date picker UIeditorFactorycreates a portal-based editor that overlays the cellregisterCellTyperegisters the custom cell type for use in column config
Create the Date Format Helper
This helper corrects user input to match the expected date format:
const correctFormat = (value, dateFormat) => {const dateFromDate = moment(value);const dateFromMoment = moment(value, dateFormat);const isAlphanumeric = value.search(/[A-Za-z]/g) > -1;let date;if ((dateFromDate.isValid() && dateFromDate.format('x') === dateFromMoment.format('x')) ||!dateFromMoment.isValid() ||isAlphanumeric) {date = dateFromDate;} else {date = dateFromMoment;}return date.format(dateFormat);}What’s happening:
- Tries to parse the value both as a native date and using Moment.js with the given format
- Picks the best interpretation and reformats it to the target
dateFormat
Create the Renderer
We reuse the built-in
autocompleterenderer, which displays a dropdown arrow icon indicating the cell has a picker:renderer: getRenderer('autocomplete')Create the Validator
The validator checks whether the entered value is a valid date and optionally auto-corrects the format:
validator: function(value, callback) {let valid = true;if (value === null || value === undefined) {value = '';}let isValidFormat = moment(value, this.dateFormat, true).isValid();let isValidDate = moment(new Date(value)).isValid() || isValidFormat;if (this.allowEmpty && value === '') {isValidDate = true;isValidFormat = true;}if (!isValidDate) {valid = false;}if (!isValidDate && isValidFormat) {valid = true;}if (isValidDate && !isValidFormat) {if (this.correctFormat === true) {const correctedValue = correctFormat(value, this.dateFormat);this.instance.setDataAtCell(this.visualRow, this.visualCol, correctedValue, 'dateValidator');valid = true;} else {valid = false;}}callback(valid);}What’s happening:
- Validates the date value against the configured
dateFormatusing Moment.js - If
correctFormatis enabled, auto-corrects misformatted but valid dates - Empty values pass validation when
allowEmptyis set
- Validates the date value against the configured
Create the Editor
The editor uses
editorFactorywithposition: 'portal'to overlay a Pikaday calendar on the cell. Arrow keys navigate days/weeks in the calendar:editor: editorFactory({position: 'portal',shortcuts: [{keys: [['ArrowLeft']],callback: (editor, _event) => {editor.pikaday.adjustDate('subtract', 1);_event.preventDefault();},},{keys: [['ArrowRight']],callback: (editor, _event) => {editor.pikaday.adjustDate('add', 1);_event.preventDefault();},},{keys: [['ArrowUp']],callback: (editor, _event) => {editor.pikaday.adjustDate('subtract', 7);_event.preventDefault();},},{keys: [['ArrowDown']],callback: (editor, _event) => {editor.pikaday.adjustDate('add', 7);_event.preventDefault();},},],init(editor) {editor.parentDestroyed = false;editor.input = editor.hot.rootDocument.createElement('input');editor.datePicker = editor.container;editor.hot.rootDocument.addEventListener('mousedown', (event) => {if (event.target && event.target.classList.contains('pika-day')) {editor.hideDatepicker(editor);}});},afterOpen(editor, event) {const cellRect = editor.TD.getBoundingClientRect();editor.input.style.width = `${cellRect.width}px`;editor.input.style.height = `${cellRect.height}px`;editor.showDatepicker(editor, event);},afterClose(editor) {if (editor.pikaday.destroy) {editor.pikaday.destroy();}},getValue(editor) {return editor.input.value;},setValue(editor, value) {editor.input.value = value;},getDateFormat(editor) {return editor.cellProperties.dateFormat ?? 'DD/MM/YYYY';},// ... getDatePickerConfig, showDatepicker, hideDatepicker// (see the full example above for complete implementation)}),What’s happening:
initcreates the input element and binds the Pikaday containerafterOpensizes the input to match the cell dimensions, then opens the date pickerafterClosedestroys the Pikaday instance to prevent memory leaks- Arrow key shortcuts navigate the calendar (left/right = day, up/down = week)
Style the Editor Input
The editor input needs CSS to match Handsontable’s native editor appearance. Without this, the input shows default browser borders and focus styles:
.ht_editor_visible > input {width: 100%;height: 100%;box-sizing: border-box !important;border: none;border-radius: 0;outline: none;margin-top: -1px;margin-left: -1px;box-shadow: inset 0 0 0 var(--ht-cell-editor-border-width, 2px)var(--ht-cell-editor-border-color, #1a42e8),0 0 var(--ht-cell-editor-shadow-blur-radius, 0) 0var(--ht-cell-editor-shadow-color, transparent) !important;background-color: var(--ht-cell-editor-background-color, #ffffff) !important;padding: var(--ht-cell-vertical-padding, 4px)var(--ht-cell-horizontal-padding, 8px) !important;border: none !important;font-family: inherit;font-size: var(--ht-font-size);line-height: var(--ht-line-height);}.ht_editor_visible > input:focus-visible {border: none !important;outline: none !important;}Key styling:
margin-top: -1pxandmargin-left: -1pxalign the editor precisely over the cell border- Uses Handsontable’s CSS custom properties (
--ht-cell-editor-*) to match the theme inset box-shadowreplaces the default border for a consistent editor highlightborder: noneandoutline: noneremove default browser focus styles
Register and Use in Handsontable
registerCellType('moment-date', cellDateTypeDefinition);const hotOptions: Handsontable.GridSettings = {data,colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Restock Date', 'Cost'],autoRowSize: true,rowHeaders: true,height: 'auto',width: '100%',autoWrapRow: true,headerClassName: 'htLeft',columns: [{ data: 'itemName', type: 'text', width: 130 },{ data: 'category', type: 'text', width: 120 },{ data: 'leadEngineer', type: 'text', width: 150 },{data: 'restockDate',type: 'moment-date',width: 150,dateFormat: 'YYYY-MM-DD',correctFormat: true,datePickerConfig: {firstDay: 0,showWeekNumber: true,disableDayFn(date) {return date.getDay() === 0 || date.getDay() === 6;},},},{data: 'cost',type: 'numeric',width: 120,className: 'htRight',numericFormat: {pattern: '$0,0.00',culture: 'en-US',},},],licenseKey: 'non-commercial-and-evaluation',};const hot = new Handsontable(container, hotOptions);Key configuration:
type: 'moment-date'- uses the custom cell type on the Restock Date columndateFormat: 'YYYY-MM-DD'- the Moment.js format string for parsing and displaycorrectFormat: true- automatically reformats valid dates to the expected formatdatePickerConfig- passed directly to Pikaday (e.g., disable weekends withdisableDayFn)
How It Works - Complete Flow
- Initial Render: Cell displays the date value with a dropdown arrow (autocomplete renderer)
- User clicks cell: The portal editor opens with an input sized to the cell and a Pikaday calendar below it
- Date selection: User picks a date from the calendar or types a value; arrow keys navigate the picker
- Validation: Moment.js checks the format and date validity; auto-corrects if
correctFormatis enabled - Save: Valid values are saved to the cell; invalid values are rejected