Skip to content

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.

TypeScript
/* 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 */
HTML
<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 dateFormat options per column
  • Disables weekend selection via datePickerConfig

Prerequisites

Terminal window
npm install moment @handsontable/pikaday
  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';
    import '@handsontable/pikaday/css/pikaday.css';
    registerAllModules();

    Why this matters:

    • moment handles date parsing, validation, and formatting
    • Pikaday provides the calendar date picker UI
    • @handsontable/pikaday/css/pikaday.css provides the calendar panel styling
    • editorFactory creates a portal-based editor that overlays the cell
    • registerCellType registers the custom cell type for use in column config
  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
  3. Create the Renderer

    The example reuses the built-in autocomplete renderer, which displays a dropdown arrow icon indicating the cell has a picker:

    renderer: getRenderer('autocomplete')
  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 dateFormat using Moment.js
    • If correctFormat is enabled, auto-corrects misformatted but valid dates
    • Empty values pass validation when allowEmpty is set
  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',
    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:

    • init creates the input element and binds the Pikaday container
    • afterOpen sizes the input to match the cell dimensions, then opens the date picker
    • afterClose destroys the Pikaday instance to prevent memory leaks
    • Arrow key shortcuts navigate the calendar (left/right = day, up/down = week)
  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: -1px and margin-left: -1px align the editor precisely over the cell border
    • Uses Handsontable’s CSS custom properties (--ht-cell-editor-*) to match the theme
    • inset box-shadow replaces the default border for a consistent editor highlight
    • border: none and outline: none remove default browser focus styles
  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',
    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 column
    • dateFormat: 'YYYY-MM-DD' - the Moment.js format string for parsing and display
    • correctFormat: true - automatically reformats valid dates to the expected format
    • datePickerConfig - passed directly to Pikaday (e.g., disable weekends with disableDayFn)

How It Works - Complete Flow

  1. Initial Render: Cell displays the date value with a dropdown arrow (autocomplete renderer)
  2. User clicks cell: The portal editor opens with an input sized to the cell and a Pikaday calendar below it
  3. Date selection: User picks a date from the calendar or types a value; arrow keys navigate the picker
  4. Validation: Moment.js checks the format and date validity; auto-corrects if correctFormat is enabled
  5. 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.