Moment.js-based date
This tutorial shows you how to create a custom date cell type using Moment.js and the Pikaday calendar picker, with format auto-correction and per-column configuration.
/* file: app.component.ts */import { Component, ChangeDetectionStrategy, ElementRef, ViewChild } from '@angular/core';import { HotCellEditorAdvancedComponent, HotCellRendererAdvancedComponent, GridSettings, HotTableModule} from '@handsontable/angular-wrapper';import moment from 'moment';import Pikaday from '@handsontable/pikaday';import '@handsontable/pikaday/css/pikaday.css';
const DATE_FORMAT = 'YYYY-MM-DD';
// `useMoment()` exists at runtime but is missing from the Pikaday type definitions.type PikadayWithMoment = Pikaday & { useMoment(momentFunction: typeof moment): void };
@Component({ standalone: true, selector: 'example1-moment-date-editor', template: `<div #container class="pikaday-container"></div>`, styles: [ ` :host { display: block; } .pikaday-container { width: 290px; } `, ],})export class MomentDateEditorComponent extends HotCellEditorAdvancedComponent<string> { override position = 'portal' as const; @ViewChild('container', { static: true }) container!: ElementRef; private pikaday: Pikaday | null = null;
override afterOpen(): void { this.pikaday = new Pikaday({ field: this.container.nativeElement, container: this.container.nativeElement, bound: false, format: DATE_FORMAT, firstDay: 0, showWeekNumber: true, disableDayFn: (date: Date) => date.getDay() === 0 || date.getDay() === 6, onSelect: (date: Date) => { this.setValue(moment(date).format(DATE_FORMAT)); }, onClose: () => { this.finishEdit.emit(); }, });
(this.pikaday as PikadayWithMoment).useMoment(moment);
if (this.value) { const m = moment(this.value, DATE_FORMAT, true);
if (m.isValid()) { this.pikaday.setMoment(m, true); } } else { this.pikaday.gotoToday(); }
this.pikaday.show(); }
override afterClose(): void { this.pikaday?.destroy(); this.pikaday = null; }
override onFocus(): void { this.container.nativeElement.focus(); }}
@Component({ standalone: true, selector: 'example1-moment-date-renderer', changeDetection: ChangeDetectionStrategy.OnPush, template: `<span>{{ formattedDate }}</span>`,})export class MomentDateRendererComponent extends HotCellRendererAdvancedComponent<string> { get formattedDate(): string { if (!this.value) return ''; const m = moment(this.value, DATE_FORMAT, true);
return m.isValid() ? m.format(DATE_FORMAT) : this.value; }}
@Component({ standalone: true, imports: [HotTableModule], selector: 'example1-moment-date', template: `<div><hot-table [data]="data" [settings]="gridSettings"></hot-table></div>`,})export class AppComponent { readonly data = [ { itemName: 'Lunar Core', category: 'Lander', leadEngineer: 'Ellen Ripley', restockDate: '2025-08-01', cost: 350000, }, { itemName: 'Zero Thrusters', category: 'Propulsion', leadEngineer: 'Sam Bell', restockDate: '2025-09-15', cost: 450000, }, { itemName: 'EVA Suits', category: 'Equipment', leadEngineer: 'Alex Rogan', restockDate: '2025-10-05', cost: 150000, }, { itemName: 'Solar Panels', category: 'Energy', leadEngineer: 'Dave Bowman', restockDate: '2025-11-10', cost: 75000, }, { itemName: 'Comm Array', category: 'Communication', leadEngineer: 'Louise Banks', restockDate: '2025-12-20', cost: 125000, }, { itemName: 'Habitat Dome', category: 'Shelter', leadEngineer: 'Dr. Ryan Stone', restockDate: '2026-01-25', cost: 1000000, }, ];
readonly gridSettings: GridSettings = { 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', width: 150, allowInvalid: false, editor: MomentDateEditorComponent, renderer: MomentDateRendererComponent, } as any, { data: 'cost', type: 'numeric', width: 120, className: 'htRight', locale: 'en-US', numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2, }, }, ], };}/* end-file */
/* file: app.config.ts */import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';import { registerAllModules } from 'handsontable/registry';import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
registerAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<div> <example1-moment-date></example1-moment-date></div>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';import '@handsontable/pikaday/css/pikaday.css';registerAllModules();Why this matters:
momenthandles date parsing, validation, and formattingPikadayprovides the calendar date picker UI@handsontable/pikaday/css/pikaday.cssprovides the calendar panel stylingeditorFactorycreates 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
The example reuses 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',shortcutsGroup: 'customEditor',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',locale: 'en-US',numericFormat: {style: 'currency',currency: 'USD',minimumFractionDigits: 2,},},],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
What you learned
You created a custom Moment.js-based date cell type in Handsontable. You used editorFactory with position: 'portal' to overlay a Pikaday calendar, Moment.js for date validation and format auto-correction, and registerCellType to make the cell type reusable across columns.
Next steps
- Pikaday - A standalone Pikaday date picker recipe that also serves as a migration path from the built-in date cell type.
- Moment.js time - The same Moment.js pattern applied to time values.
- Flatpickr - An alternative date picker using the Flatpickr library with dark theme support.