Angular MaterialのDatepickerでISO 8601以外のフォーマットの日付を扱う方法
はじめに
Angular Materialにカレンダーから日付を選択するDatepickerという部品がある。ただ、このDatepickerには厄介なところがあって、日付をISO 8601でしか管理できない。なので、フォームとして日付を例えば yyyy/MM/dd
で管理したい場合に "部品の外部で" 自前で変換を差し込む必要がある。そしてここでもう一つ厄介なことに、DatepickerはValueAccessorをProvideしているため、同じ <input>
に自作のDirective等でValueAccessorを追加でProvideすることができない。つまり、ValueAccessorの onChange
や writeValue
でよしなに値を書き換えるという方法は取れない。
最近このような状況があり、いくつか考えた結果割とシンプルに上記課題を解決する方法を思いついたのでまとめておく。
解決方法
実装の方針
以下の方針で実装した。
- Datepickerをバインドした
<input>
の値がISO 8601になってしまうのはもう諦めて、別途用意した<input hidden>
に影響を押し込める - 欲しいフォーマットの日付を入力するための
<input>
も用意し、formControl
はこちらと結びつける - Datepickerはカレンダーだけ利用させていただき、あとはイベントを拾ってそれを
formControl
にpatchValue
する - Datepickerのカレンダーを開いたときにformControlの値と同期が取れていてほしいので、
<input hidden>
の[value]
にはformControl.value
(今回フォーマットはyyyy/MM/dd
の想定)をISO 8601に変換してバインドする
最後に上記をComponentとして共通化したものを紹介する。
共通Component化
これから作るComponentは <mat-form-field>
の中に入るが、<mat-fom-field>
は直接の子要素として MatFormFieldControl
を実装したComponent(例えば <input>
)を持つ必要があり、単にすべての <input>
を共通Componentに押し込めてしまうとこの制約にひっかかる。
MatFormFieldControl
は<mat-form-field>
内で使えるinputを実装するためのinterfaceで結構制約が強い。このレベルの単純な機能を共通化するために実装するのはコストが見合わないのでそこまではやらないことにした。
そこで、改めて考えるとそもそも本質的にフォームとして働くのは formControl
をバインドした <input>
であって、Datepickerをバインドした <input hidden>
の方はおまけである。よって、フォームとして働く方の <input>
にはそのまま残ってもらって、あくまでもカレンダー系部品をまとめる存在として共通Componentを定義する。この共通Componentはカレンダーからの入力で formControl
に値をセットしないといけないので、@Input()
デコレータで外から formControl
だけもらえるようにしておく。
最終的な実装が以下である。
共通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>
共通Componentを利用する側
こうする必然性はないが、formGroup
から簡単に日付入力フォームのcontrolを参照できるようにgetterを定義しておく。直接 formControl
を参照しても悪くない。共通Component側のinterfaceは AbstractControl
を受け取るようにしたのでどちらでもいけるようになっている。
.tsget birthday(): AbstractControl | null {
return this.sampleForm.get('birthday');
}
.html<mat-form-field appearance="fill">
<mat-label>誕生日</mat-label>
<input
type="text"
matInput
placeholder="誕生日"
id="birthday"
name="birthday"
required
formControlName="birthday"
/>
<app-datepicker matSuffix [control]="birthday"></app-datepicker>
</mat-form-field>
あくまでも表面に出ているところはただのtextの <input>
なのでかなり扱いやすい。冒頭で少し触れた <input>
でValueAccessorをProvideする、ということも可能になり、以下の記事で紹介した ngx-mask
による入力値のフィルタリング等とも組み合わせることができる。