DateRangePicker参戦!
この記事は Angular Advent Calendar 2020 の 18 日目の記事です。
はじめに
この記事ではAngular 10で追加されたDateRangePickerを用いる中で、イベントハンドリングについてつまづいたところを紹介していきます。
toBのデスクトップアプリケーションを開発していると、日付の範囲を設定する時がやたらあります。私もAngular 8でtoBシステムを作っている時にどうしてもMaterialDesignベースのDateRangePickerを使用したかったのですが、当時は対応がされておらず、DatePickerを二つつなげてDateRangePickerっぽいものを作ったりしていました。
今回しばらくAngularバージョンを更新できていなかったプロダクトがあったのですが、DateRangePickerを使うユースケースが新規で発生したため、どうしても使いたいモチベーションによって、バージョンを上げることができましたw
実装したもの
実装したものはDateRangePickerの日付範囲選択イベントをもとにAPIリクエストを投げて、帰ってきたデータをもとに、グラフライブラリを用いてグラフを作成する機能です。
最初はDateRangePickerならよしなにイベント発火されて良い感じにリクエストできるだろうと思ってサンプル通りに実装しました。
<!-- 細かいところは省略 -->
<mat-form-field appearance="fill">
<mat-label>Enter a date range</mat-label>
<mat-date-range-input
[formGroup]="range"
[rangePicker]="picker"
>
<input
matStartDate
formControlName="start"
placeholder="Start date"
(dateChange)="addEvent('[start]change', $event)"
>
<input
matEndDate
formControlName="end"
placeholder="End date"
(dateChange)="addEvent('[end]change', $event)"
>
</mat-date-range-input>
<mat-datepicker-toggle
matSuffix
[for]="picker"
></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
しかし、こちらの (dateChange) イベントはStartDateをカレンダー選択した時、EndDateを選択した時にそれぞれ2回ずつイベントが発火してしまいます。
これが原因で不要なリクエストが何回か送られてしまっていました。
こちらを解消するためにドキュメント Customizing the date selection logic にもあった通り、
@Component({
selector: 'app-custom-strategy-date-range-picker',
templateUrl: './custom-strategy-date-range-picker.component.html',
styleUrls: ['./custom-strategy-date-range-picker.component.css'],
providers: [{
provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
useClass: MyMatCalendarRangeStrategy
}]
})
DIにて独自に定義した `MyMatCalendarRangeStrategy` をInjectionして、以下のようにSubjectからObservableをStartDateとEndDateが揃ったときのみ発火するようにしました。
@Injectable()
export class MyMatCalendarRangeStrategy<D> implements MatDateRangeSelectionStrategy<D> {readonly isComplete = new Subject<{start: D, end: D}>();constructor(private _dateAdapter: DateAdapter<D>) {}selectionFinished(date: D, currentRange: DateRange<D>) {
let {start, end} = currentRange;if (start == null) {
start = date;
} else if (end == null && date && this._dateAdapter.compareDate(date, start) >= 0) {
end = date;
} else {
start = date;
end = null;
}if (end != null && start != null && this._dateAdapter.compareDate(date, start) >= 0) {
this.isComplete.next({start: start, end: end});
}return new DateRange<D>(start, end);
}createPreview(activeDate: D | null, currentRange: DateRange<D>) {
//
}
}
これにより、カレンダーによる日付範囲の選択を行った場合、もともと書いていたコードに違和感を残すことなく、発火を一回に絞ることができました!
しかし、これだと日付をキーボードで直接入力した際はイベントが発火されませんでした。
これを皮切りに、ドキュメントに書いていない裏ルート的にInjectionできる場所をDateRangePickerの実装を見ながら、綺麗な方法を探してみましたが、最終的にはIssueにもあった通り、 (dataChange)のイベント内でStartDate, EndDateがどちらも揃った時だけリクエストを行うようにするのがシンプルかつ最適だと判断してコードは以下のように落ち着いています。。
onDateChange() {
const start = this.range.get('start')?.value as Date;
const end = this.range.get('end')?.value as Date;if (start && end) {
this.events.push(`[start, end]:
${start?.toLocaleDateString()},
${end?.toLocaleDateString()}`
);
}
}
比較コードは以下のリポジトリにまとめました!
https://github.com/koiizuka/date-range-picker
まとめ
今回長らくIssueが対応されるのを待っていたDateRangePickerを(ちょっとつまづいたものの)使ってみて、プロダクションリリースもすることができました。
軽くつまづいてまぁまぁ時間かけて調べても、最適解が当時どこにも定義されてなかったので、せっかくだしということで今回記事にしました。私が調べた限りだと (dateChange)イベント内でのチェックが最適だと思われます。(もしもっとエレガントな方法があれば教えて欲しいです!!!)
去年のAdventCalendar で紹介したTypedFormsも ロードマップに追加 されるなど着々と必要なものを追加していってくださるAngularチームには感謝の思いが止まりません。
実はまだDateRangePickerは完全ではなく、デスクトップアプリケーションにおけるDateRangePickerはGoogle Analyticsのような操作感がMaterial Designで定義されています。
今後こちらのデザインパターンに関してもサポートされていくようにはなっていくようですが、今回 MAT_DATE_RANGE_SELECTION_STRATEGY のカスタマイズを通して、Angular Componentsの中のコードをちょっと詳しめに見たりしたので、どうやったらこのデザインパターンを実現できるかも考えてみようかなと思ったりしています!(実際のコード見るの大事)
明日は ic_lifewood さんです!!