Date picker
Overview
This guide shows how to create a custom date picker cell using Angular components with the native HTML5 date input. You’ll learn how to build custom editor and renderer components that extend Handsontable’s Angular wrapper classes, providing a clean, type-safe implementation.
Difficulty: Intermediate
Time: ~15 minutes
Libraries: date-fns
What You’ll Build
A cell that:
- Displays formatted dates (e.g., “12/31/2024” or “31/12/2024”)
- Opens a native HTML5 date picker when edited
- Supports per-column configuration (EU vs US date formats)
- Uses Angular component architecture with proper lifecycle management
- Leverages Angular’s change detection and two-way data binding
- Auto-saves when a date is selected
Prerequisites
npm install date-fnsEnsure you have @handsontable/angular-wrapper installed in your Angular project.
Import Dependencies
import { Component, ChangeDetectorRef, ChangeDetectionStrategy, inject, ViewChild, ElementRef } from "@angular/core";import {GridSettings,HotCellEditorAdvancedComponent,HotCellRendererAdvancedComponent,} from "@handsontable/angular-wrapper";import { format, parse, isValid } from "date-fns";Key imports explained:
- Angular Core: Component decorators, change detection, dependency injection
- Handsontable Wrapper: Base classes for custom editor and renderer components
- date-fns: Lightweight date formatting and parsing
format: Convert Date to formatted stringparse: Parse string to Date objectisValid: Validate Date objects
Define Date Formats
const DATE_FORMAT_US = "MM/dd/yyyy";const DATE_FORMAT_EU = "dd/MM/yyyy";Why constants?
- Reusability across renderer and column configuration
- Single source of truth
- Easy to add more formats (ISO, custom, etc.)
Create the Renderer Component
The renderer displays the date in a human-readable format using an Angular component.
@Component({selector: "date-renderer",changeDetection: ChangeDetectionStrategy.OnPush,template: ` <div>{{ formattedDate }}</div>`,standalone: false,})export class DateRendererComponent extends HotCellRendererAdvancedComponent<string, { renderFormat: string }> {get formattedDate(): string {return format(new Date(this.value), this.getProps().renderFormat);}}What’s happening:
- Extends HotCellRendererAdvancedComponent: Generic types specify:
TValue = string: The cell value type (date as string)TProps = { renderFormat: string }: Custom renderer properties
- @Input() value: Automatically provided by Handsontable (inherited from base class)
- getProps(): Returns
rendererPropsfrom column configuration - OnPush strategy: Optimizes change detection for better performance
Key benefits:
- Type-safe access to custom properties via
getProps() - Automatic change detection when
valuechanges - Clean Angular template syntax
- One component definition, multiple configurations per column
Adding error handling:
get formattedDate(): string {if (!this.value) return '';try {const date = new Date(this.value);return isValid(date)? format(date, this.getProps().renderFormat || 'MM/dd/yyyy'): 'Invalid date';} catch (e) {return 'Invalid date';}}- Extends HotCellRendererAdvancedComponent: Generic types specify:
Create the Editor Component (Part 1 - Setup)
The editor component handles user input with a native HTML5 date picker.
@Component({selector: "date-editor",changeDetection: ChangeDetectionStrategy.OnPush,template: `<inputtype="date"[(ngModel)]="dateValue"#editorInput(change)="onDateChange()"style="width: 100%; height: 100%; padding: 8px; border: 2px solid #4CAF50; border-radius: 4px; font-size: 14px; box-sizing: border-box;"/>`,standalone: false,})export class DateEditorComponent extends HotCellEditorAdvancedComponent<string> {dateValue: string = "";@ViewChild("editorInput", { static: true })protected editorInput!: ElementRef<HTMLInputElement>;private readonly cdr = inject(ChangeDetectorRef);// Lifecycle methods continue in next step...}What’s happening:
- Extends
HotCellEditorAdvancedComponent<string>: Base class provides:@Input() originalValue: Cell’s current value@Input() row, column, prop: Cell position@Input() cellProperties: Column configuration@Output() finishEdit: Emit to save changes@Output() cancelEdit: Emit to discard changesgetValue()/setValue(): Value management
- Template: Native HTML5
<input type="date">with:[(ngModel)]: Two-way binding todateValue#editorInput: Template reference for DOM access(change): Triggers when user selects a date
- @ViewChild: Direct access to input element for focus management
- ChangeDetectorRef: Manual change detection control
- OnPush strategy: Optimized rendering
- Extends
Editor - Lifecycle Hook
afterOpenCalled immediately after the editor opens.
override afterOpen(): void {setTimeout(() => {this.editorInput.nativeElement.showPicker?.();}, 0);}What’s happening:
showPicker()is a native HTML5 API that opens the date picker calendarsetTimeoutensures the DOM is fully rendered before opening picker?.optional chaining handles browsers that don’t supportshowPicker()- Provides smooth UX - calendar appears automatically
Editor - Lifecycle Hook
beforeOpenCalled before the editor opens to initialize the value.
override beforeOpen(_: any, { originalValue }: any) {if (originalValue) {try {let parsedDate: Date;// Try to parse MM/DD/YYYY formatif (typeof originalValue === "string" && originalValue.includes("/")) {parsedDate = parse(originalValue, "MM/dd/yyyy", new Date());}// Try to parse YYYY-MM-DD formatelse if (typeof originalValue === "string" && originalValue.includes("-")) {parsedDate = parse(originalValue, "yyyy-MM-dd", new Date());}// Fallback to generic date parsingelse {parsedDate = new Date(originalValue);}if (isValid(parsedDate)) {// Format as YYYY-MM-DD for native input type="date"this.dateValue = format(parsedDate, "yyyy-MM-dd");} else {this.dateValue = "";}} catch (error) {console.error("Error parsing date:", error);this.dateValue = "";}} else {this.dateValue = "";}this.cdr.detectChanges();}What’s happening:
- Receives
originalValuefrom the cell (stored format: MM/dd/yyyy) - Parses the value using
parse()from date-fns - Handles multiple date formats (MM/DD/YYYY, YYYY-MM-DD)
- Converts to YYYY-MM-DD format required by
<input type="date"> - Validates with
isValid()before setting - Triggers change detection to update the view
Why multiple format support?
- Cell stores dates in MM/dd/yyyy format
- Native input requires YYYY-MM-DD format
- Ensures compatibility with different data sources
- Receives
Editor - Date Change Handler
Called when user selects a date in the picker.
onDateChange(): void {if (this.dateValue) {try {// Parse YYYY-MM-DD from inputconst parsedDate = parse(this.dateValue, "yyyy-MM-dd", new Date());if (isValid(parsedDate)) {// Format as MM/DD/YYYY for Handsontableconst formattedDate = format(parsedDate, "MM/dd/yyyy");this.setValue(formattedDate);}} catch (error) {console.error("Error formatting date:", error);}}}What’s happening:
- Triggered by
(change)event in template - Parses native input value (YYYY-MM-DD)
- Converts to storage format (MM/dd/yyyy)
- Calls
setValue()to update editor’s internal value - Value will be saved when editor closes
Format conversion flow:
- User sees: “12/31/2024” (renderer displays)
- Editor opens: “2024-12-31” (native input requires)
- User selects: Native picker updates
dateValue onDateChange: Converts back to “12/31/2024”- Saved to cell: “12/31/2024”
- Triggered by
Configure Grid with Multiple Date Formats
const DATE_FORMAT_US = "MM/dd/yyyy";const DATE_FORMAT_EU = "dd/MM/yyyy";@Component({selector: "app-date-picker-example",standalone: false,template: ` <div><hot-table [data]="data" [settings]="gridSettings"></hot-table></div>`,})export class DatePickerExampleComponent {readonly data = [{ id: 640329, itemName: "Lunar Core", restockDate: "2025-08-01" },{ id: 863104, itemName: "Zero Thrusters", restockDate: "2025-09-15" },{ id: 395603, itemName: "EVA Suits", restockDate: "2025-10-05" },];readonly gridSettings: GridSettings = {autoRowSize: true,rowHeaders: true,autoWrapRow: true,height: "auto",manualColumnResize: true,manualRowResize: true,colHeaders: ["ID", "Item Name", "Restock Date EU", "Restock Date US"],columns: [{ data: "id", type: "numeric" },{ data: "itemName", type: "text" },// European format column{data: "restockDate",width: 150,allowInvalid: false,rendererProps: {renderFormat: DATE_FORMAT_EU,},editor: DateEditorComponent,renderer: DateRendererComponent,},// US format column{data: "restockDate",width: 150,allowInvalid: false,rendererProps: {renderFormat: DATE_FORMAT_US,},editor: DateEditorComponent,renderer: DateRendererComponent,},],};}Key configuration points:
- editor: DateEditorComponent: Reference to your editor component class
- renderer: DateRendererComponent: Reference to your renderer component class
- rendererProps: Custom properties passed to
getProps()in renderer- Type-safe access via generic parameter
- Different format per column
- Same data source: Both columns display
restockDate - Different presentation: EU (dd/MM/yyyy) vs US (MM/dd/yyyy)
Amazing feature:
- One data column (
restockDate) - Two visual representations
- Same editor and renderer components
- Configuration-driven behavior
Module Configuration
Register components in your Angular module:
import { NgModule, ApplicationConfig } from "@angular/core";import { BrowserModule } from "@angular/platform-browser";import { registerAllModules } from "handsontable/registry";import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from "@handsontable/angular-wrapper";import { CommonModule } from "@angular/common";import { NON_COMMERCIAL_LICENSE } from "@handsontable/angular-wrapper";import { FormsModule } from "@angular/forms";import { DatePickerExampleComponent, DateEditorComponent, DateRendererComponent } from "./app.component";// Register Handsontable's modulesregisterAllModules();export const appConfig: ApplicationConfig = {providers: [{provide: HOT_GLOBAL_CONFIG,useValue: {license: NON_COMMERCIAL_LICENSE,} as HotGlobalConfig,},],};@NgModule({imports: [BrowserModule, HotTableModule, CommonModule, FormsModule],declarations: [DatePickerExampleComponent, DateEditorComponent, DateRendererComponent],providers: [...appConfig.providers],bootstrap: [DatePickerExampleComponent],})export class AppModule {}Important notes:
- FormsModule: Required for
[(ngModel)]in editor template - Component declarations: Both DateEditorComponent and DateRendererComponent must be declared in the NgModule declarations array
- registerAllModules(): Registers all Handsontable features
- HOT_GLOBAL_CONFIG: Global configuration for all tables in the app
- FormsModule: Required for
Advanced Enhancements
Time Picker Support
Add time selection with
datetime-localinput:// In DateEditorComponent template<inputtype="datetime-local"[(ngModel)]="dateValue"#editorInput(change)="onDateChange()"/>// Update parsing in beforeOpenif (isValid(parsedDate)) {this.dateValue = format(parsedDate, "yyyy-MM-dd'T'HH:mm");}// Update rendering formatrenderFormat: 'dd/MM/yyyy HH:mm'Date Validation with Min/Max
Add native HTML5 validation:
// In DateEditorComponent template<inputtype="date"[(ngModel)]="dateValue"[min]="minDate"[max]="maxDate"#editorInput/>// In component class@Input() minDate: string = '2024-01-01';@Input() maxDate: string = '2024-12-31';// Pass via cellProperties in column configcolumns: [{data: 'restockDate',editor: DateEditorComponent,minDate: '2024-01-01',maxDate: '2024-12-31',}]Custom Styling with Angular
Use component styles:
@Component({selector: "date-editor",template: `<input type="date" [(ngModel)]="dateValue" #editorInput class="date-input" />`,styles: [`.date-input {width: 100%;height: 100%;padding: 8px;border: 2px solid #4CAF50;border-radius: 4px;font-size: 14px;box-sizing: border-box;}.date-input:focus {outline: none;border-color: #45a049;box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);}`],standalone: false,})Error Handling with Visual Feedback
Add validation messages:
@Component({template: `<div class="editor-container"><input type="date" [(ngModel)]="dateValue" #editorInput (change)="onDateChange()" />@if (hasError) {<span class="error-message">Invalid date format</span>}</div>`,})export class DateEditorComponent extends HotCellEditorAdvancedComponent<string> {hasError = false;onDateChange(): void {try {const parsedDate = parse(this.dateValue, "yyyy-MM-dd", new Date());this.hasError = !isValid(parsedDate);if (!this.hasError) {const formattedDate = format(parsedDate, "MM/dd/yyyy");this.setValue(formattedDate);}} catch (error) {this.hasError = true;}}}Reactive Forms Integration
Use Angular’s reactive forms:
import { FormControl, ReactiveFormsModule } from "@angular/forms";@Component({template: `<input type="date" [formControl]="dateControl" #editorInput />`,standalone: false,})// Note: Import ReactiveFormsModule in your NgModuleexport class DateEditorComponent extends HotCellEditorAdvancedComponent<string> {dateControl = new FormControl("");override beforeOpen(_: any, { originalValue }: any) {if (originalValue) {const parsedDate = parse(originalValue, "MM/dd/yyyy", new Date());if (isValid(parsedDate)) {this.dateControl.setValue(format(parsedDate, "yyyy-MM-dd"));}}this.dateControl.valueChanges.subscribe((value) => {if (value) {const parsedDate = parse(value, "yyyy-MM-dd", new Date());if (isValid(parsedDate)) {this.setValue(format(parsedDate, "MM/dd/yyyy"));}}});}}Internationalization (i18n)
Use Angular’s built-in i18n:
import { DatePipe } from "@angular/common";@Component({selector: "date-renderer",template: `<div>{{ value | date : getProps().renderFormat }}</div>`,standalone: false,})export class DateRendererComponent extends HotCellRendererAdvancedComponent<string, { renderFormat: string }> {}// Note: DatePipe is available through CommonModule imported in your NgModule
Congratulations! You’ve created a production-ready date picker with full localization support and advanced configuration.