How to use Datepicker in Angular Material to handle dates in formats other than ISO 8601
Overview
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.
Solution
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 theformControl
to this. - Use Datepicker only for the calendar UI, and manually picking up events to execute
patchValue
offormControl
. - For synchronization between Datepicker calendar and the formControl's value, bind
formControl.value
to[value]
attribute of<input hidden>
after converting format fromyyyy/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
datepicker.component.tsimport { 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 (!event.target.value) return;
this.control?.setValue(
this.convertToYYYYMMDDFromISODate(event.target.value?.toISOString())
);
this.control?.markAsTouched();
this.control?.markAsDirty();
};
public dateInputHandler = (event: MatDatepickerInputEvent<Date>) => {
if (!event.target.value) return;
this.control?.setValue(
this.convertToYYYYMMDDFromISODate(event.target.value?.toISOString())
);
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;
}
}
datepicker.component.html<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.
.tsget birthday(): AbstractControl | null {
return this.sampleForm.get('birthday');
}
.html<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.