Angular Data GridNative Date Picker Cell - Step-by-Step Guide

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 string
      • parse: Parse string to Date object
      • isValid: 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 rendererProps from column configuration
    • OnPush strategy: Optimizes change detection for better performance

    Key benefits:

    • Type-safe access to custom properties via getProps()
    • Automatic change detection when value changes
    • 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 changes
      • getValue() / setValue(): Value management
    • Template: Native HTML5 <input type="date"> with:
      • [(ngModel)]: Two-way binding to dateValue
      • #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 calendar
    • setTimeout ensures the DOM is fully rendered before opening picker
    • ?. optional chaining handles browsers that don't support showPicker()
    • 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 originalValue from 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:

    1. User sees: "12/31/2024" (renderer displays)
    2. Editor opens: "2024-12-31" (native input requires)
    3. User selects: Native picker updates dateValue
    4. onDateChange: Converts back to "12/31/2024"
    5. 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.