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

投稿日: 11/16/2021

はじめに

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

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

解決方法

実装の方針

以下の方針で実装した。

  • Datepickerをバインドした <input> の値がISO 8601になってしまうのはもう諦めて、別途用意した <input hidden> に影響を押し込める
  • 欲しいフォーマットの日付を入力するための <input> も用意し、formControl はこちらと結びつける
  • Datepickerはカレンダーだけ利用させていただき、あとはイベントを拾ってそれを formControlpatchValue する
  • 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.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する、ということも可能になり、以下の記事で紹介した ngx-mask による入力値のフィルタリング等とも組み合わせることができる。

https://silurus.dev/articles/1PCqZpPS6qNIm1MFX9TjFM