How to use Datepicker in Angular Material to handle dates in formats other than ISO 8601

Posted on: November 16, 2021


Angular Material has a component called Datepicker that enables us to select a date from a calendar UI. However, this Datepicker has an annoying problem: it can only manage dates in ISO 8601 format. So if you want to manage dates in, for example, yyyy/MM/dd format, you need to insert your own conversions "outside the component". Another annoying thing here is that since Datepicker provides a ValueAccessor, it is not possible to provide an additional ValueAccessor in the same <input> by yourself. In other words, it is not possible to control the behavior of the form by your customized ValueAccessor that has customized onChange or writeValue.

Recently, I had a situation like this, and after some thought, I came up with a rather simple way to solve the above problem.


Implementation Policy

The implementation policy is as follows.

  • Giving up on the fact that the value of <input> to which Datepicker is bound is ISO 8601 format, push that effect to a separately prepared <input hidden>.
  • Have an another <input> for entering dates in the format we want, and bind the formControl to this.
  • Use Datepicker only for the calendar UI, and manually picking up events to execute patchValue of formControl.
  • For synchronization between Datepicker calendar and the formControl's value, bind formControl.value to [value] attribute of <input hidden> after converting format from yyyy/MM/dd to ISO 8601.

Finally, I introduce a common Component of the above.

Commonalize by Component

The Component we are going to create will be inside <mat-form-field>, but <mat-fom-field> must have a Component that implements MatFormFieldControl (e.g. <input>) as a direct child element, and if we simply push all <input> into a common Component, it will be viplated by this restriction.

MatFormFieldControl is an interface to implement inputs that can be used in <mat-form-field>, and it is quite restrictive. I decided not to go that far because it is not worth the cost to implement this Interface in order to make this level of simple functionality common.

When I think about it again, it is the <input> bound with formControl that essentially works as a form, and the <input hidden> bound with Datepicker is an extra. Therefore, let the <input> that bound with formControl remain as it is, and define a common component as the entity that organizes calendar UI components. Since this common component needs to set the value of formControl by the input from the calendar UI, we make the common component can get formControl from outside with the @Input() decorator.

The final implementation is shown below.

Implement Common Component

import { Component, Input } from '@angular/core'; import { AbstractControl, FormControl } from '@angular/forms'; import { MatDatepickerInputEvent } from '@angular/material/datepicker'; export const ISO_8601_REGEX = /^(\d{4})-(\d{2})-(\d{2})(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))?)?$/; export const CUSTOM_YMD_REGEX = /^(\d{4})\/(\d{2})\/(\d{2})$/; @Component({ selector: 'app-datepicker', templateUrl: './datepicker.component.html', }) export class DatepickerComponent { @Input() control?: AbstractControl | null; public dateChangeHandler = (event: MatDatepickerInputEvent<Date>) => { if (! return; this.control?.setValue( this.convertToYYYYMMDDFromISODate( ); this.control?.markAsTouched(); this.control?.markAsDirty(); }; public dateInputHandler = (event: MatDatepickerInputEvent<Date>) => { if (! return; this.control?.setValue( this.convertToYYYYMMDDFromISODate( ); this.control?.markAsTouched(); this.control?.markAsDirty(); }; public convertToISODateFromYYYYMMDD(value: string): string { const found = value.match(CUSTOM_YMD_REGEX); if (!found || found[0] !== value) return value; return new Date( Number(found[1]), Number(found[2]) - 1, Number(found[3]) ).toISOString(); } private convertToYYYYMMDDFromISODate(value: string): string | null { if (ISO_8601_REGEX.test(value)) { const date = new Date(value); if (!isNaN(date.getTime())) { date.setMinutes(date.getMinutes() + 540); return date?.toISOString().replace(ISO_8601_REGEX, `$1/$2/$3`); } } return value; } }
<input type="hidden" [matDatepicker]="picker" [value]="convertToISODateFromYYYYMMDD(control?.value)" (dateChange)="dateChangeHandler($event)" (dateInput)="dateInputHandler($event)" /> <mat-datepicker-toggle [for]="picker"></mat-datepicker-toggle> <mat-datepicker #picker></mat-datepicker>

Use Common Component

There is no need to do this, but define a getter so that you can easily refer to the control of a date input form from formGroup. It is not bad to refer to formControl directly. Since the interface of the common component is designed to receive AbstractControl, either is fine.

get birthday(): AbstractControl | null { return this.sampleForm.get('birthday'); }
<mat-form-field appearance="fill"> <mat-label>Birthday</mat-label> <input type="text" matInput placeholder="Birthday" id="birthday" name="birthday" required formControlName="birthday" /> <app-datepicker matSuffix [control]="birthday"></app-datepicker> </mat-form-field>

It is quite easy to handle, because the part that appears on the surface is just a text <input>. Also, it is possible to provide a ValueAccessor for <input>, which I mentioned at the beginning of this article, or combine it with ngx-mask to filter input values.

share on...