【Angular】Reactive Forms でテンプレートからコンポーネントに渡す値の型を指定したい
はじめに
Angular で Web アプリ開発を行う際、入力フォームを Reactive Forms (リアクティブフォーム) で構築することがよくあります。
一例を示すと、 FormGroup を使用 (form
という名前の FormGroup を作成) して、テンプレート側に Text 型の Input 要素 (<input type="text" formControlName="hoge">
) を配置し、コンポーネント側でその値を取得する (this.form.controls.hoge.value
) という流れになります。
最近とあるプロジェクトで Reactive Forms を使用していて、テンプレート側に配置した様々な Input 要素から指定した型 (string
, number
, boolean
, Date
) 通りの値を取得したいというオレオレニーズがありましたので、検証してみました。
動作環境
OS | Version |
---|---|
Ubuntu | 22.04 LTS |
※ Windows 10 Pro の Windows Subsystem for Linux (WSL 2) を使用
Environment | Version |
---|---|
Node.js | 20.1.0 |
npm | 9.6.4 |
※ Docker Hub の Official Image の node:20
を使用
Package | Version |
---|---|
@angular/cli | 16.0.0 |
@angular/material | 16.0.4 |
前提として
Typed Forms
Angular ではこれまで Reactive Forms でテンプレートから取得した値は、すべて any
型としてコンポーネントに渡されていました。
せっかく TypeScript でコードを書いているのに…と思う方が多かったからか、 Angular v14 でとうとう Typed Forms が導入され、 Reactive Forms から any
と null
を駆逐することができるようになりました!
※ Template-driven Form (テンプレート駆動フォーム) には、まだ適用できません…
これまで通りのコードの書き方をすると、コンポーネント側で与えられた初期値の型に応じて FormControl の型推論が行われます。
また、 FormControl のコンストラクタにオプションとして {nonNullable: true}
を渡すか、 NonNullableFormBuilder クラスを使用することにより、 null
を許容しない (厳密には FormControl#reset 実行時、 null
ではなく FormControl のコンストラクタに渡した初期値でリセットする) ことが可能になります。
検証結果
いきなり結論ですが、各型の値を取得する方法をまとめました。
入力フォームの各 Input 要素の値に null
を許容するか否かでケースを分けています。
Null を許容しない場合
Component
- app.component.ts
import { Component, inject } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { private readonly fb = inject(FormBuilder); readonly form = this.fb.nonNullable.group({ str: ["", Validators.required], num: [0, Validators.required], bool: [false, Validators.required], date: [new Date(), Validators.required], }); dispTypes(): void { console.log(this.form.value); } cancelStep(e: KeyboardEvent): void { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); } } }
NonNullableFormBuilder クラスを使用して、 FormGroup 内のすべての FormControl に {nonNullable: true}
を適用します。これで FormGroup 内の FormControl に、ユニオン型として null
型が追加されなくなります。
あとは各 FormControl の初期値に、適切な型の値を設定するだけです。
Template
- app.component.html
<form [formGroup]="form"> <input type="text" formControlName="str"> <input type="number" class="input-num" formControlName="num" (keydown)="cancelStep()"> <input type="checkbox" formControlName="bool"> <mat-form-field> <input matInput [matDatepicker]="picker" formControlName="date" /> <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle> <mat-datepicker #picker></mat-datepicker> </mat-form-field> <button (click)="dispTypes()"> </form>
- app.component.scss
.input-num::-webkit-outer-spin-button, .input-num::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; -moz-appearance: textfield; appearance: none; }
Number 型の Input 要素 (<input type="number">
) は、使用する Web ブラウザにより数値を上下するスピンボタンが UI に表示されますが、スピンボタンを使用しない場合は CSS で非表示にすることができます。
また、上矢印キー (ArrowUp
) と下矢印キー (ArrowDown
) の keydown
イベントで Event#preventDefault を実行すれば、入力値の加減算処理をキャンセルすることが可能です。
この辺りはアプリの仕様によりますので、お好みで設定してください。
Console
{str: 'abc', num: 123, bool: true, date: Sun Jun 11 2023 12:15:12 GMT+0900 (日本標準時)}
これでそれぞれの Input 要素から、狙った通りの型の値を取得することができました。異論は認めない。
Null を許容する場合
Component
- app.component.ts
import { Component, inject } from '@angular/core'; import { FormBuilder } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { private readonly fb = inject(FormBuilder); readonly form = this.fb.group<{ str: string; num: number | null; bool: boolean; date: Date | null; }>({ str: [""], num: [null], bool: [false], date: [null], }); dispTypes(): void { console.log(this.form.value); } cancelStep(e: KeyboardEvent): void { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); } } }
NonNullableFormBuilder クラスは使用せず、 FromBuilder#group へジェネリクスの型引数として各 FormControl の型を指定します。
この際、 string
型と boolean
型については初期値として ''
(空文字) や false
(未チェック) を渡すことが可能 (UI に影響を与えない) なので、 null
を許容するのは厳密には number
型と Date
型になります。
Template
"Null を許容しない場合" の "Template" と同様なので、省略します。
Console
{str: 'abc', num: 123, bool: true, date: Sun Jun 11 2023 12:32:51 GMT+0900 (日本標準時)}
こちらも Null を許容しない場合と同様、狙った通りの型の値を取得することができました。
ただし、 Number 型の Input 要素と Angular Material の Datepicker が未入力の場合、または FormControl#reset などを実行した場合、コンポーネント側で取得できる値は null
になります。
まとめ (言い訳)
分かります。
え? Native じゃない Element 混じっているけど?ですよね。
仰る通り、 Input 要素には Date 型 (<input type="date">
) が確かに存在するのですが、検証の結果、 Reactive Forms で取得した値は string
型でした。
いや、そうは言っても Angular で日付の入力フォームを作るなら、どうせ Angular Material の Datepicker 使うでしょ?
無いんだよ! Date
型を返す Input 要素は無いんだよ!!
Date
型って、そもそも JavaScript のプリミティブな型じゃないしな!!!
はい、論破~。
すいません、はい、 Symbol はプリミティブです。すいません。ありません。調子に乗りました。 BigInt …ごめんなさい。勘弁してください。
おわりに
今回は Reactive Forms でテンプレートからコンポーネントへ任意の型の値を渡す方法をまとめました。
ここ最近は Web 開発をするならフロントエンドは Angular でってことが多いので、プロジェクトを経る度に新しい学び、発見があります。
Angular のバージョンもいつの間にか 16 まで進んでいて、僕が Angular の勉強を始めた頃はまだ 7 とか 8 だったような記憶…
一時期、 React に浮気しそうになったことがありましたが、長く続けてきたからこそ、少しずつできることが増えてきました。
よく Angular は初心者には敷居が高い、学習コストが高いなどと言われますが、一度しっかり基礎を学んで自分の中でベストプラクティスを作ってしまえば、普段の開発ではあまり悩むことは無いと思います。
むしろ、かっちりとした書き方が定められるのでチーム開発には有利だと思いますし、 Google 主導のコミュニティによって今も積極的にメンテナンスされているので、ビジネスの場においては一定の安心感があります。
まだまだ Angular を使う機会はありそうので、 ChatGPT に仕事を奪われるその日まで、 Angular でフロントエンドを書き続けます。
弊社はフロントエンドに Angular を使用した Web アプリ開発の実績が豊富 (当社比) です。
お仕事のご依頼・ご相談、または、一緒に Web アプリの開発がしたいぜって方は、是非ご連絡をお待ちしています (*•̀ᴗ•́*)و