AI can fly !!

AI がやりたい Web エンジニアのアウトプット (AI の知識は無い)

【Angular】Reactive Forms でテンプレートからコンポーネントに渡す値の型を指定したい

Angular

はじめに

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 から anynull を駆逐することができるようになりました!

※ Template-driven Form (テンプレート駆動フォーム) には、まだ適用できません…

angular.jp

これまで通りのコードの書き方をすると、コンポーネント側で与えられた初期値の型に応じて 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 アプリの開発がしたいぜって方は、是非ご連絡をお待ちしています (*•̀ᴗ•́*)و