Angular Data GridNative Date Picker Cell - Step-by-Step Guide
- Overview
- What You'll Build
- Prerequisites
- Complete Example
- Step 1: Import Dependencies
- Step 2: Define Date Formats
- Step 3: Create the Renderer Component
- Step 4: Create the Editor Component (Part 1 - Setup)
- Step 5: Editor - Lifecycle Hook afterOpen
- Step 6: Editor - Lifecycle Hook beforeOpen
- Step 7: Editor - Date Change Handler
- Step 8: Configure Grid with Multiple Date Formats
- Step 9: Module Configuration
- Advanced Enhancements
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-fns
Ensure you have @handsontable/angular-wrapper installed in your Angular project.
Complete Example
Step 1: 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
Step 2: 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.)
Step 3: 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';
}
}
Step 4: 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: `
<input
type="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
Step 5: Editor - Lifecycle Hook afterOpen
Called 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
Step 6: Editor - Lifecycle Hook beforeOpen
Called 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 format
if (typeof originalValue === "string" && originalValue.includes("/")) {
parsedDate = parse(originalValue, "MM/dd/yyyy", new Date());
}
// Try to parse YYYY-MM-DD format
else if (typeof originalValue === "string" && originalValue.includes("-")) {
parsedDate = parse(originalValue, "yyyy-MM-dd", new Date());
}
// Fallback to generic date parsing
else {
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
Step 7: Editor - Date Change Handler
Called when user selects a date in the picker.
onDateChange(): void {
if (this.dateValue) {
try {
// Parse YYYY-MM-DD from input
const parsedDate = parse(this.dateValue, "yyyy-MM-dd", new Date());
if (isValid(parsedDate)) {
// Format as MM/DD/YYYY for Handsontable
const 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"
Step 8: 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
Step 9: 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 modules
registerAllModules();
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
Advanced Enhancements
1. Time Picker Support
Add time selection with datetime-local input:
// In DateEditorComponent template
<input
type="datetime-local"
[(ngModel)]="dateValue"
#editorInput
(change)="onDateChange()"
/>
// Update parsing in beforeOpen
if (isValid(parsedDate)) {
this.dateValue = format(parsedDate, "yyyy-MM-dd'T'HH:mm");
}
// Update rendering format
renderFormat: 'dd/MM/yyyy HH:mm'
2. Date Validation with Min/Max
Add native HTML5 validation:
// In DateEditorComponent template
<input
type="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 config
columns: [
{
data: 'restockDate',
editor: DateEditorComponent,
minDate: '2024-01-01',
maxDate: '2024-12-31',
}
]
3. 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,
})
4. 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;
}
}
}
5. 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 NgModule
export 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"));
}
}
});
}
}
6. 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.