Search code examples
angularangular-materialmat-datepicker

Can not set form value to string if matDatepicker is set


<mat-form-field>
    <mat-label>Choose a date</mat-label>
    <input matInput [matDatepicker]="picker" formControlName="date">
    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-datepicker #picker></mat-datepicker>
</mat-form-field>

If I set the input value to a date everything works fine.

this.form.controls['date'].setValue(new Date());

enter image description here

But I also want to manully set the input value to a string like this

this.form.controls['date'].setValue('permanent');

But MatDatepicker does not allow me to do that and shows nothing (it probably expects a date format) even though my form accepts the string value 'permanent'. form:

enter image description here

input:

enter image description here

What I was expecting:

enter image description here


Solution

  • Result

    after use custom date adapter

    By default, the mat-datepicker is using NativeDateAdapter as the default DateAdapter.

    You can create a custom date adapter class by extending from DateAdapter.

    For example, I created custom-date-adapter.ts and extend DateAdapter with type param <Date | string> which allow support of string.

    export class CustomDateAdapter extends DateAdapter<Date | string> {
    

    Inject and use the default NativeDateAdapter when overriding DateAdapter abstract methods

      adapter: NativeDateAdapter;
      ...
      constructor(nativeDateAdapter: NativeDateAdapter) {
      ...
          this.adapter = nativeDateAdapter;
    

    For example, for parse and isDateInstance override, check for permanent keyword, if true, then return. Else, call the nativeDateAdapter default method.

    NativeDateAdapter@isDateInstance is called when setValue('permanent')

    and

    NativeDateAdapter@parse is called when type inpermanent.

      parse(value: any, parseFormat: any): string | Date | null {
        if (value === 'permanent') {
          return 'permanent';
        }
        return this.adapter.parse(value, parseFormat);
      }
      isDateInstance(obj: any): boolean {
        if (obj === 'permanent') {
          return true;
        }
        return this.adapter.isDateInstance(obj);
      }
    

    Other small details like the private _today: Date = new Date();, this variable is used in other override methods, to pass into nativeDateAdapter method when the user enters any string, so that the date picker can expand and point to today date.

    For example, if return 0 in getYear method, the picklist will point to 0 years when the user enters string input and expand it.


    Full code

    app.component.html

    <form [formGroup]="form">
      <mat-form-field>
        <mat-label>Choose a date</mat-label>
        <input matInput [matDatepicker]="picker" formControlName="date">
        <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
        <mat-datepicker #picker></mat-datepicker>
    </mat-form-field>
    
      <hr>
    
      date.value = {{ form.controls['date'].value }}
    
      <br/>
    
      date.valid = {{ form.controls['date'].valid }}
    </form>
    
    

    app.component.ts

    import { Component } from '@angular/core';
    import { FormControl, FormGroup } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.scss']
    })
    export class AppComponent {
      title = 'ng-date-picker';
      form = new FormGroup({
        date: new FormControl('permanent')
      });
    }
    

    app.module.ts

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent } from './app.component';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    import { MatFormFieldModule } from '@angular/material/form-field';
    import { MatDatepickerModule } from '@angular/material/datepicker';
    import {
      DateAdapter,
      MatNativeDateModule,
      MAT_DATE_FORMATS,
      MAT_NATIVE_DATE_FORMATS,
      NativeDateAdapter,
    } from '@angular/material/core';
    import { MatInputModule } from '@angular/material/input';
    import { CustomDateAdapter } from './custom-date-adapter';
    
    import { MatButtonModule } from '@angular/material/button';
    
    @NgModule({
      declarations: [AppComponent],
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        MatFormFieldModule,
        MatDatepickerModule,
        MatNativeDateModule,
        FormsModule,
        MatInputModule,
        MatButtonModule,
      ],
      providers: [
        NativeDateAdapter,
        { provide: DateAdapter, useClass: CustomDateAdapter },
      ],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    

    custom-date-adapter.ts

    import { Injectable } from '@angular/core';
    import {
      DateAdapter, NativeDateAdapter
    } from '@angular/material/core';
    
    @Injectable()
    export class CustomDateAdapter extends DateAdapter<Date | string> {
      adapter: NativeDateAdapter;
      private _today: Date = new Date();
    
      constructor(nativeDateAdapter: NativeDateAdapter) {
        super();
        this.adapter = nativeDateAdapter;
      }
    
      parse(value: any, parseFormat: any): string | Date | null {
        if (value === 'permanent') {
          return 'permanent';
        }
        return this.adapter.parse(value, parseFormat);
      }
      isDateInstance(obj: any): boolean {
        if (obj === 'permanent') {
          return true;
        }
        return this.adapter.isDateInstance(obj);
      }
      isValid(date: string | Date): boolean {
        return date instanceof Date
          ? this.adapter.isValid(date)
          : date === 'permanent';
      }
      getYear(date: string | Date): number {
        return date instanceof Date
          ? this.adapter.getYear(date)
          : this.adapter.getYear(this._today);
      }
      getMonth(date: string | Date): number {
        return date instanceof Date
          ? this.adapter.getMonth(date)
          : this.adapter.getMonth(this._today);
      }
      getDate(date: string | Date): number {
        return date instanceof Date
          ? this.adapter.getDate(date)
          : this.adapter.getDate(this._today);
      }
      getDayOfWeek(date: string | Date): number {
        return date instanceof Date
          ? this.adapter.getDayOfWeek(date)
          : this.adapter.getDayOfWeek(this._today);
      }
      getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {
        return this.adapter.getMonthNames(style);
      }
      getDateNames(): string[] {
        return this.adapter.getDateNames();
      }
      getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
        return this.adapter.getDayOfWeekNames(style);
      }
      getYearName(date: string | Date): string {
        return date instanceof Date
          ? this.adapter.getYearName(date)
          : this.adapter.getYearName(this._today);
      }
      getFirstDayOfWeek(): number {
        return this.adapter.getFirstDayOfWeek();
      }
      getNumDaysInMonth(date: string | Date): number {
        return date instanceof Date
          ? this.adapter.getNumDaysInMonth(date)
          : this.adapter.getNumDaysInMonth(this._today);
      }
      clone(date: string | Date): string | Date {
        return date instanceof Date
          ? this.adapter.clone(date)
          : this.adapter.clone(this._today);
      }
      createDate(year: number, month: number, date: number): string | Date {
        return this.adapter.createDate(year, month, date);
      }
      today(): string | Date {
        return this.adapter.today();
      }
      format(date: string | Date, displayFormat: any): string {
        return date instanceof Date
          ? this.adapter.format(date, displayFormat)
          : (date as string);
      }
      addCalendarYears(date: string | Date, years: number): string | Date {
        return date instanceof Date
          ? this.adapter.addCalendarYears(date, years)
          : '';
      }
      addCalendarMonths(date: string | Date, months: number): string | Date {
        return date instanceof Date
          ? this.adapter.addCalendarMonths(date, months)
          : '';
      }
      addCalendarDays(date: string | Date, days: number): string | Date {
        return date instanceof Date ? this.adapter.addCalendarDays(date, days) : '';
      }
      toIso8601(date: string | Date): string {
        return date instanceof Date ? this.adapter.toIso8601(date) : '';
      }
      invalid(): string | Date {
        return this.adapter.invalid();
      }
    }
    

    package.json

    {
      "name": "ng-date-picker",
      "version": "0.0.0",
      "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "build": "ng build",
        "watch": "ng build --watch --configuration development",
        "test": "ng test"
      },
      "private": true,
      "dependencies": {
        "@angular/animations": "^15.0.0",
        "@angular/cdk": "^15.0.1",
        "@angular/common": "^15.0.0",
        "@angular/compiler": "^15.0.0",
        "@angular/core": "^15.0.0",
        "@angular/forms": "^15.0.0",
        "@angular/material": "^15.0.1",
        "@angular/platform-browser": "^15.0.0",
        "@angular/platform-browser-dynamic": "^15.0.0",
        "@angular/router": "^15.0.0",
        "rxjs": "~7.5.0",
        "tslib": "^2.3.0",
        "zone.js": "~0.12.0"
      },
      "devDependencies": {
        "@angular-devkit/build-angular": "^15.0.2",
        "@angular/cli": "~15.0.2",
        "@angular/compiler-cli": "^15.0.0",
        "@types/jasmine": "~4.3.0",
        "jasmine-core": "~4.5.0",
        "karma": "~6.4.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage": "~2.2.0",
        "karma-jasmine": "~5.1.0",
        "karma-jasmine-html-reporter": "~2.0.0",
        "typescript": "~4.8.2"
      }
    }