Angular MaterialのDatepickerでISO 8601以外のフォーマットの日付を扱う方法

投稿日時:11/16/2021, 4:33 PM

はじめに

Angular Materialにカレンダーから日付を選択する Datepickerという部品がある。ただ、このDatepickerには厄介なところがあって、日付をISO 8601でしか管理できない。なので、フォームとして日付を例えば yyyy/MM/dd で管理したい場合に "部品の外部で" 自前で変換を差し込む必要がある。そしてここでもう一つ厄介なことに、DatepickerはValueAccessorをProvideしているため、同じ <input> に自作のDirective等でValueAccessorを追加でProvideすることができない。

最近こういう状況があり、いくつか考えた結果割とシンプルに上記課題を解決する方法を思いついたのでまとめておく。

解決方法

実装の方針

以下の方針で実装した。

  • Datepickerをバインドした <input> の値がISO 8601になってしまうのはもう諦めて、別途用意した <input hidden> に影響を押し込める
  • 欲しいフォーマットの日付を入力するための <input> も用意し、formControl はこちらと結びつける
  • Datepickerはカレンダーだけ利用させていただき、あとはイベントを拾ってそれを formControlpatchValue する
  • Datepickerのカレンダーを開いたときにformControlの値と同期が取れていてほしいので、<input hidden>[value] には formControl.value(今回フォーマットは yyyy/MM/dd の想定)をISO 8601に変換してバインドする

シンプルなので上記だけで実装は十分伝わると思うが最後に共通部品化したものを紹介する。

共通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.ts
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 (!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 を受け取るようにしたのでどちらでもいけるようになっている。

.ts
get 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する、ということも当然可能である。


share on...