JavaScript Data GridMoment.js date Cell Type - Step-by-Step Guide
Overview
This guide shows how to create a custom date cell type using the Moment.js (opens new window) library. Users can format dates using the Moment.js API.
Difficulty: Beginner
Time: ~25 minutes
Libraries: moment, @handsontable/pikaday
Complete Example
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/pikaday
Step 1: Import 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
Step 2: 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
Step 3: Create the Renderer
We reuse the built-in autocomplete renderer, which displays a dropdown arrow icon indicating the cell has a picker:
renderer: getRenderer('autocomplete')
Step 4: 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
Step 5: Create the Editor
The editor uses editorFactory with position: '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)
Step 6: 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) 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;
}
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
Step 7: 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