Moment.js-based time
This tutorial shows you how to create a custom time cell type using Moment.js for validation and format auto-correction.
import { HotTable, HotColumn } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import { getRenderer } from 'handsontable/renderers';import { getEditor } from 'handsontable/editors';import { registerCellType } from 'handsontable/cellTypes';import moment from 'moment';
registerAllModules();
const cellTimeTypeDefinition = { renderer: getRenderer('text'), validator(value, callback) { const timeFormat = this.timeFormat ?? 'h:mm:ss a'; let valid = true;
if (value === null) { value = ''; }
value = /^\d{3,}$/.test(value) ? parseInt(value, 10) : value;
const twoDigitValue = /^\d{1,2}$/.test(value);
if (twoDigitValue) { value += ':00'; }
const date = moment( value, [ 'YYYY-MM-DDTHH:mm:ss.SSSZ', 'X', 'x', ], true ).isValid() ? moment(value) : moment(value, timeFormat);
let isValidTime = date.isValid(); let isValidFormat = moment(value, timeFormat, true).isValid() && !twoDigitValue;
if (this.allowEmpty && value === '') { isValidTime = true; isValidFormat = true; }
if (!isValidTime) { valid = false; }
if (!isValidTime && isValidFormat) { valid = true; }
if (isValidTime && !isValidFormat) { if (this.correctFormat === true) { const correctedValue = date.format(timeFormat);
this.instance.setDataAtCell(this.visualRow, this.visualCol, correctedValue, 'timeValidator'); valid = true; } else { valid = false; } }
callback(valid); }, editor: getEditor('text'),};
registerCellType('moment-time', cellTimeTypeDefinition);
/* 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', time: '09:30', 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', time: '14: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', time: '08:00', 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', time: '16:45', 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', time: '11: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', time: '23:00', operationalStatus: 'Operational', },];/* end:skip-in-preview */
const ExampleComponent = () => { return ( <HotTable data={data} colHeaders={['Item Name', 'Category', 'Lead Engineer', 'Arrival Time', 'Cost']} autoRowSize={true} rowHeaders={true} height="auto" width="100%" autoWrapRow={true} headerClassName="htLeft" licenseKey="non-commercial-and-evaluation" > <HotColumn data="itemName" type="text" width={130} /> <HotColumn data="category" type="text" width={120} /> <HotColumn data="leadEngineer" type="text" width={150} /> <HotColumn data="time" type="moment-time" width={150} timeFormat="HH:mm" correctFormat={true} /> <HotColumn data="cost" type="numeric" width={120} className="htRight" locale="en-US" numericFormat={{ style: 'currency', currency: 'USD', minimumFractionDigits: 2 }} /> </HotTable> );};
export default ExampleComponent;import { HotTable, HotColumn } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import { getRenderer } from 'handsontable/renderers';import { getEditor } from 'handsontable/editors';import { registerCellType } from 'handsontable/cellTypes';import moment from 'moment';
registerAllModules();
const cellTimeTypeDefinition = { renderer: getRenderer('text'), validator(this: any, value: string | number, callback: (valid: boolean) => void) { const timeFormat: string = this.timeFormat ?? 'h:mm:ss a'; let valid = true;
if (value === null) { value = ''; }
value = /^\d{3,}$/.test(String(value)) ? parseInt(String(value), 10) : value;
const twoDigitValue = /^\d{1,2}$/.test(String(value));
if (twoDigitValue) { value = `${value}:00`; }
const date = moment( value, [ 'YYYY-MM-DDTHH:mm:ss.SSSZ', 'X', 'x', ], true ).isValid() ? moment(value) : moment(value, timeFormat);
let isValidTime = date.isValid(); let isValidFormat = moment(value, timeFormat, true).isValid() && !twoDigitValue;
if (this.allowEmpty && value === '') { isValidTime = true; isValidFormat = true; }
if (!isValidTime) { valid = false; }
if (!isValidTime && isValidFormat) { valid = true; }
if (isValidTime && !isValidFormat) { if (this.correctFormat === true) { const correctedValue = date.format(timeFormat);
this.instance.setDataAtCell(this.visualRow, this.visualCol, correctedValue, 'timeValidator'); valid = true; } else { valid = false; } }
callback(valid); }, editor: getEditor('text'),};
registerCellType('moment-time', cellTimeTypeDefinition);
/* start:skip-in-preview */interface InventoryItem { id: number; itemName: string; itemNo: string; leadEngineer: string; cost: number; inStock: boolean; category: string; itemQuality: number; origin: string; quantity: number; valueStock: number; repairable: boolean; supplierName: string; time: string; operationalStatus: string;}
const data: InventoryItem[] = [ { 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', time: '09:30', 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', time: '14: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', time: '08:00', 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', time: '16:45', 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', time: '11: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', time: '23:00', operationalStatus: 'Operational', },];/* end:skip-in-preview */
const ExampleComponent = () => { return ( <HotTable data={data} colHeaders={['Item Name', 'Category', 'Lead Engineer', 'Arrival Time', 'Cost']} autoRowSize={true} rowHeaders={true} height="auto" width="100%" autoWrapRow={true} headerClassName="htLeft" licenseKey="non-commercial-and-evaluation" > <HotColumn data="itemName" type="text" width={130} /> <HotColumn data="category" type="text" width={120} /> <HotColumn data="leadEngineer" type="text" width={150} /> <HotColumn data="time" type="moment-time" width={150} timeFormat="HH:mm" correctFormat={true} /> <HotColumn data="cost" type="numeric" width={120} className="htRight" locale="en-US" numericFormat={{ style: 'currency', currency: 'USD', minimumFractionDigits: 2 }} /> </HotTable> );};
export default ExampleComponent;Overview
This guide shows how to create a custom time cell type using the Moment.js library. Users can format times using the Moment.js API.
Difficulty: Beginner
Time: ~15 minutes
Libraries: moment
What You’ll Build
A cell that:
- Displays time values as formatted text
- Accepts
timeFormatoptions for customization (e.g.,HH:mm,h:mm:ss a) - Validates time input using Moment.js
- Auto-corrects time format when
correctFormatis enabled
Prerequisites
npm install momentImport Dependencies
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { getRenderer } from 'handsontable/renderers';import { getEditor } from 'handsontable/editors';import { registerCellType } from 'handsontable/cellTypes';import moment from 'moment';registerAllModules();Why this matters:
momenthandles time parsing, validation, and formattinggetRenderer('text')andgetEditor('text')reuse Handsontable’s built-in text renderer and editorregisterCellTyperegisters the custom cell type for use in column config
Create the Renderer
The example reuses the built-in
textrenderer, which displays the time value as plain text:renderer: getRenderer('text')Create the Validator
The validator parses the input using Moment.js and checks it against the configured
timeFormat. It handles Unix timestamps, two-digit shorthand (e.g.,9becomes9:00), and auto-correction:validator: function(value, callback) {const timeFormat = this.timeFormat ?? 'h:mm:ss a';let valid = true;if (value === null) {value = '';}value = /^\d{3,}$/.test(value) ? parseInt(value, 10) : value;const twoDigitValue = /^\d{1,2}$/.test(value);if (twoDigitValue) {value += ':00';}const date = moment(value, ['YYYY-MM-DDTHH:mm:ss.SSSZ','X', // Unix timestamp'x' // Unix ms timestamp], true).isValid() ?moment(value) : moment(value, timeFormat);let isValidTime = date.isValid();// is it in the specified formatlet isValidFormat = moment(value, timeFormat, true).isValid() && !twoDigitValue;if (this.allowEmpty && value === '') {isValidTime = true;isValidFormat = true;}if (!isValidTime) {valid = false;}if (!isValidTime && isValidFormat) {valid = true;}if (isValidTime && !isValidFormat) {if (this.correctFormat === true) {const correctedValue = date.format(timeFormat);this.instance.setDataAtCell(this.visualRow, this.visualCol, correctedValue, 'timeValidator');valid = true;} else {valid = false;}}callback(valid);}What’s happening:
- Converts numeric-only input (3+ digits) to integers for Unix timestamp parsing
- Appends
:00to 1-2 digit values (e.g.,9becomes9:00) - Tries ISO 8601 and Unix timestamp formats first, then falls back to the configured
timeFormat - If
correctFormatis enabled, auto-corrects valid but misformatted times
Create the Editor
The example reuses the built-in
texteditor — a text input for editing the time value:editor: getEditor('text')Complete Cell Type Definition
Put all the pieces together and register the cell type:
const cellTimeTypeDefinition = {renderer: getRenderer('text'),validator: function(value, callback) {// ... validator code from Step 3},editor: getEditor('text'),};registerCellType('moment-time', cellTimeTypeDefinition);What’s happening:
- renderer: Uses the built-in
textrenderer to display the time value - validator: Custom validator that validates and optionally corrects time format using Moment.js
- editor: Uses the built-in
texteditor for text input - registerCellType: Registers the
moment-timecell type for use in column config
- renderer: Uses the built-in
Use in Handsontable
registerCellType('moment-time', cellTimeTypeDefinition);const hotOptions: Handsontable.GridSettings = {data,colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Arrival Time', '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: 'time',type: 'moment-time',width: 150,timeFormat: 'HH:mm',correctFormat: true,},{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-time'- uses the custom cell type on the Arrival Time columntimeFormat: 'HH:mm'- the Moment.js format string for 24-hour timecorrectFormat: true- automatically reformats valid times to the expected formatheaderClassName: 'htLeft'- left-aligns all column headers
How It Works - Complete Flow
- Initial Render: Cell displays the time value as plain text using the
textrenderer - User clicks cell: The built-in text editor opens for editing
- User enters time: Input like
9,14:30, or a Unix timestamp is accepted - Validation: Moment.js checks the format and time 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 time cell type in Handsontable. You used Moment.js to validate and auto-correct time values, reused the built-in text renderer and editor, and registered the result with registerCellType for use across columns.
Next steps
- Moment.js date - The same Moment.js pattern applied to date values, with a Pikaday calendar picker.
- Numbro - A custom numeric cell type using the Numbro formatting library.
- Flatpickr - A date picker using Flatpickr with dark theme support.