0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Angularを学ぶ フォーム

Last updated at Posted at 2025-08-16

フォーム

Angularでは、ユーザーのログインやプロフィール更新などのデータ入力を扱うために「フォーム」を使う。フォームの処理には2つの方法がある。

  • リアクティブフォーム
    • プログラムでフォームの構造を定義・管理する方法
    • フォームの状態や検証をコードで細かく制御できる
  • テンプレート駆動フォーム
    • HTMLテンプレート内でフォームを定義する方法
    • シンプルで直感的に使いやすいが、大規模な制御は難しい

アプローチの選択

リアクティブフォームとテンプレート駆動フォームは、データ管理の方法が異なり、それぞれに異なる利点がある。

フォーム 特徴
リアクティブフォーム フォームのオブジェクトモデルに直接アクセス可能。堅牢でスケーラブル、再利用性やテスト性が高い。重要なフォームやリアクティブパターン使用時に適する。
テンプレート駆動フォーム テンプレートのディレクティブに依存してモデルを作成・操作。簡単なフォームの追加に向くが、リアクティブフォームほどスケーラブルではない。基本的な要件やテンプレートだけで管理可能な場合に適する。

共通のフォーム基盤クラス

リアクティブフォームとテンプレート駆動フォームの両方は、次のベースクラスに基づいて構築されている。

ベースクラス 説明
FormControl 単一の入力欄の値や状態(有効/無効など)を管理する
FormGroup 複数の入力欄をまとめて値や状態を管理する
FormArray 入力欄のリスト(配列)の値や状態を管理する
ControlValueAccessor Angular のフォームと HTML 要素をつなぐ橋渡しをする

フォーム内のデータフロー

フォームを使う場合、ビューとコンポーネントのデータモデルを同期させる必要がある。ユーザーが入力するとモデルが更新され、プログラムでモデルを変更するとビューに反映される。リアクティブフォームとテンプレート駆動フォームでは、このデータの流れが異なる。

リアクティブフォームのデータフロー

リアクティブフォームでは、ビュー内の各フォーム要素は、フォームモデル(FormControl インスタンス)に直接リンクされている。 ビューからモデルへの更新、およびモデルからビューへの更新は同期であり、UIのレンダリング方法に依存しない。

ビューからモデルへのデータフロー

ユーザーが入力フィールドに入力すると、フォーム要素が inputイベントを発行し、ControlValueAccessor がその値を FormControl に即座に渡す。FormControlvalueChanges オブザーバブルを通じて更新を発行し、サブスクライバーが新しい値を受け取る流れになる。

image.png

モデルからビューへのデータフロー

setValue() を呼び出すと、FormControl の値が更新され、valueChanges オブザーバブルが新しい値を発行する。サブスクライバーはその値を受け取り、ControlValueAccessor がフォーム入力要素を最新の値に更新する流れになる。
image.png

テンプレート駆動フォームのデータフロー

テンプレート駆動フォームでは、フォームの各要素は内部的に管理されるディレクティブに接続され、ディレクティブがフォームモデルを作成・更新して値の同期を行う。

ビューからモデルへのデータフロー

ユーザーが入力フィールドに入力すると、フォーム要素は input イベントを発行し、ControlValueAccessorFormControlsetValue() を呼び出す。FormControlvalueChanges オブザーバブルで新しい値を発行し、サブスクライバーが受け取ると同時に、NgModel.viewToModelUpdate() によって ngModelChange イベントを発行し、双方向バインディングでコンポーネントのプロパティが更新される。
image.png

モデルからビューへのデータフロー

コンポーネントのプロパティが更新されると変更検知が走り、NgModel の入力変更により ngOnChanges が呼ばれる。ngOnChanges は内部の FormControl の値を設定する非同期タスクをキューに入れ、変更検知完了後にそのタスクが実行される。FormControlvalueChanges オブザーバブルで新しい値を発行し、サブスクライバーが受け取り、ControlValueAccessor がビューの入力要素を最新の値に更新する。
image.png

リアクティブフォーム

リアクティブフォームは、フォームの状態をコードで明確に管理し、不変なデータ構造で扱う方式

  • 入力の変化ごとに新しい状態を作り、一貫性を保つ
  • Observableを使って同期的に値を取得し、変化を追跡できる
  • データが予測可能で、テストしやすい

テンプレート駆動フォームはHTMLテンプレート側で直接データを変更するため、可変で非同期的な管理になり、リアクティブフォームほど明示的ではない。

フォームコントロールの追加

フォームコントロールを使用するには3つの手順がある。

コンポーネントの生成と ReactiveFormsModule のインポート

コンポーネントを ng generate component で生成し、ReactiveFormsModule をインポートする。

import {FormControl, ReactiveFormsModule} from '@angular/forms';
@Component({
    selector: 'app-name-editor',
    templateUrl: './name-editor.component.html',
    styleUrls: ['./name-editor.component.css'],
    imports: [ReactiveFormsModule],
})
export class NameEditorComponent {

FormControl をインスタンス化する

FormControl のコンストラクタで初期値(ここでは空文字)を設定し、インスタンス化する。
コンポーネントクラス内でコントロールを作成すると、フォーム入力の状態を監視・更新・検証できるようになる。

import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
@Component({
    selector: 'app-name-editor',
    templateUrl: './name-editor.component.html',
    styleUrls: ['./name-editor.component.css'],
    imports: [ReactiveFormsModule],
})
export class NameEditorComponent {
    name = new FormControl('');
...
}

テンプレートにコントロールを登録する

FormControl を作成したら、テンプレートのフォームコントロール要素に関連付ける必要がある。ReactiveFormsModule に含まれる FormControlDirectiveformControl バインディングを使うことで、コンポーネントで作成したフォームコントロールをテンプレート側の入力要素と関連付けられる。

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">

フォームコントロールの値を表示する

フォームの値は次の方法で取得できる

  • valueChanges を使って変更を監視(テンプレートでは AsyncPipe、クラスでは subscribe()
  • value プロパティで現在の値のスナップショットを取得
使い方 特徴 いつ使うか
AsyncPipe + valueChanges 値の変化をリアルタイムで購読し、自動でテンプレートを更新。購読解除も自動。 入力中の変化をそのまま画面に反映したいとき(ライブプレビューなど)
value プロパティ 現在の値を1回だけ取得するスナップショット。変化は自動反映されない。 ボタン押下時など、特定のタイミングだけ値が欲しいとき

フォームコントロールの値を置換する

リアクティブフォームでは、プログラムから値を更新できる。setValue() を使うと、フォームコントロールの値を完全に置き換え、構造に応じて検証も行う。

updateName() {
    this.name.setValue('Nancy');
}

フォームコントロールのグループ化

リアクティブフォームは、フォームグループフォーム配列という複数の関連するコントロールを単一の入力フォームにグループ化する方法を提供している。

タイプ 説明
フォームグループ 複数のコントロールをまとめて管理する固定セットのフォーム。ネストして複雑なフォームも作れる。
フォーム配列 実行時にコントロールを追加・削除できる動的フォーム。ネストも可能で、柔軟なフォーム構造を作れる。

フォームグループの作成

コンポーネントクラスに profileForm というプロパティを作り、FormGroup を使って初期化する。フォームグループには firstNamelastName の2つのフォームコントロールを追加して管理する。

import {Component} from '@angular/core';
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';
@Component({
    selector: 'app-profile-editor',
    templateUrl: './profile-editor.component.html',
    styleUrls: ['./profile-editor.component.css'],
    imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
    profileForm = new FormGroup({
        firstName: new FormControl(''),
        lastName: new FormControl(''),
        ...
    });
    ...
}

FormGroup モデルとビューの関連付け

フォームグループは、含まれる各コントロールの状態や変更を追跡する。コントロールの値が変わると、親のフォームグループも新しい状態や値を発行する。モデルが定義されたら、テンプレートを更新してビューに反映させる必要がある。

<form [formGroup]="profileForm">
    <label for="first-name">First Name: </label>
    <input id="first-name" type="text" formControlName="firstName" />
    <label for="last-name">Last Name: </label>
    <input id="last-name" type="text" formControlName="lastName" />
...
</form>

フォームグループ(profileForm)は FormGroup ディレクティブ を使ってフォーム要素にバインドされ、モデルとフォーム入力の間の通信を作る。さらに、FormControlName ディレクティブ の formControlName を使うことで、各入力要素がフォームグループ内の個別コントロールに結びつく。

フォームデータの保存

ProfileEditor コンポーネントはユーザー入力を受け取るが、フォーム値をコンポーネント外で処理する必要がある。FormGroup ディレクティブはフォームの submit イベントを監視し、ngSubmit イベントを発行する。このイベントにコールバック(例:onSubmit())をバインドして、フォーム送信時の処理を行える。

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
onSubmit() {
    console.warn(this.profileForm.value);
}

ネストされたフォームグループの作成

フォームグループは、個々のコントロールだけでなく、他のフォームグループも含められるため、複雑なフォームを論理的に小さなセクションに分けて管理できる。ネストされたフォームグループを使うと、大きなフォームを管理しやすく分割でき、例えば「名前」や「住所」の情報をそれぞれ別のグループにまとめることができる。

profileForm にネストされたグループを作るには、フォームグループ内に address というサブフォームグループを追加する。

import {Component} from '@angular/core';
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';
@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
    profileForm = new FormGroup({
        firstName: new FormControl(''),
        lastName: new FormControl(''),
        address: new FormGroup({
            street: new FormControl(''),
            city: new FormControl(''),
            state: new FormControl(''),
            zip: new FormControl(''),
        }),
    });
...
}

この例では、address グループは既存の firstNamelastName に加え、streetcitystatezip コントロールを含む。ネストされた address グループの値やステータスの変更は親の profileForm に伝わり、全体のモデルの整合性が保たれる。

データモデルの一部を更新する

複数のコントロールを持つフォームグループの値を更新する際、モデルの一部だけを更新することができる。
値を更新する方法は 2種類 ある。

メソッド 説明
setValue() フォームグループの構造に厳密に従い、すべてのコントロールの値を新しい値で置き換える。
patchValue() 指定したプロパティだけを更新し、部分的に値を置き換えることができる。
updateProfile() {
    this.profileForm.patchValue({
        firstName: 'Nancy',
        address: {
            street: '123 Drew Street',
        },
    });
}

FormBuilder サービスを使用してコントロールを生成する

複数のフォームを扱うとき、手動でフォームコントロールを作ると作業が煩雑になる。
FormBuilder サービス を使うと、コントロールを簡単に生成できる便利なメソッドが提供される。
使用するには3つの手順がある。

FormBuilder クラスのインポート

@angular/forms パッケージから FormBuilder クラスをインポートする。

import {FormBuilder, ReactiveFormsModule} from '@angular/forms';

FormBuilder サービスの注入

FormBuilder サービスはリアクティブフォームモジュールで提供される注入可能なプロバイダーであり、inject() 関数を使ってコンポーネントに注入できる。

private formBuilder = inject(FormBuilder);

フォームコントロールの生成

FormBuilder サービスには control()group()array() の3つのメソッドがあり、コンポーネントクラスでフォームコントロール、フォームグループ、フォーム配列を簡単に生成できる。
group() メソッドを使って、profileForm を作成できる。

profileForm = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    address: this.formBuilder.group({
        street: [''],
        city: [''],
        state: [''],
        zip: [''],
    }),
    ...
});

フォーム入力の検証

フォーム検証は、ユーザー入力が正しく完全であることを確認するために使う。ここでは、フォームコントロールに単一のバリデーターを追加し、フォーム全体のステータスを確認する方法を説明する。

検証については後の章で詳細に記載する。

バリデーター関数のインポート

リアクティブフォームには、一般的な検証に使える バリデーター関数 が用意されている。
これらはフォームコントロールを受け取り、検証結果に応じて エラーオブジェクト または null を返す。
使用するには、@angular/forms から Validators クラスをインポートする。

import {Validators} from '@angular/forms';

フィールドを必須にする

ProfileEditor コンポーネントで、firstName コントロールを初期化する際に、配列の2番目の項目として Validators.required を追加して必須入力に設定する。

private formBuilder = inject(FormBuilder);
    profileForm = this.formBuilder.group({
        firstName: ['', Validators.required],
        lastName: [''],
        address: this.formBuilder.group({
            street: [''],
            city: [''],
            state: [''],
            zip: [''],
        }),
    ...
  });

フォームステータスの表示

フォームコントロールに必須バリデーターを追加すると、初期ステータスは 無効 になる。
この無効状態は親フォームグループにも伝わり、フォーム全体のステータスも無効になる。
フォームグループの現在のステータスは status プロパティ で取得できる。

<p>Form Status: {{ profileForm.status }}</p>

動的フォームの作成

FormArray は、名前のない複数のコントロールを管理できる動的フォーム用の仕組み。

  • コントロールを自由に追加・削除可能
  • 配列の値やステータスは子コントロールから自動で計算される
  • 事前にコントロールの数が決まっていない場合に便利

FormArray のインポート

@angular/forms から FormArray をインポートして型情報に使用する。
FormBuilder サービスを使うと、簡単に FormArray インスタンス を作成できる。

import {FormArray} from '@angular/forms';

FormArray コントロールの定義

FormArray は任意の数のコントロールで初期化でき、コントロールは配列として定義する。profileFormaliases プロパティとしてフォーム配列を追加する場合、FormBuilder.array() で配列を作成し、FormBuilder.control() で初期コントロールを設定する。

private formBuilder = inject(FormBuilder);
profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
        street: [''],
        city: [''],
        state: [''],
        zip: [''],
    }),
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
});

FormArray コントロールにアクセス

profileForm.get() を何度も呼び出す代わりに、ゲッターを使って aliases フォーム配列にアクセスできる。フォーム配列は未定義数のコントロールを管理し、ゲッター経由でアクセスすると便利で、追加されたコントロールにも簡単に対応できる。ゲッター構文を使ってクラスプロパティ aliases を作り、親フォームグループからこのフォーム配列コントロールを取得する。

get aliases() {
    return this.profileForm.get('aliases') as FormArray;
}

フォーム配列から取得されるコントロールは AbstractControl 型であるため、フォーム配列特有のメソッドにアクセスするには明示的に型を指定する必要がある。エイリアスコントロールを動的に追加するには、メソッドを定義して FormArray.push() を使い、新しいコントロールを配列に挿入する。

addAlias() {
    this.aliases.push(this.formBuilder.control(''));
}

テンプレートにフォーム配列を表示

テンプレートに反映させるには、フォーム配列インスタンスとテンプレートをバインド する必要がある。formGroupName と同様に、formArrayName を使うことで FormArrayNameDirective を介して配列と通信できる。

<div formArrayName="aliases">
    <h2>Aliases</h2>
    <button type="button" (click)="addAlias()">+ Add another alias</button>
    @for (alias of aliases.controls; track $index; let i = $index) {
        <div>
            <!-- The repeated alias template -->
            <label for="alias-{{ i }}">Alias:</label>
            <input id="alias-{{ i }}" type="text" [formControlName]="i" />
        </div>
    }
</div>

厳密に型付けされたリアクティブフォーム

リアクティブフォームでは、フォームモデルを明示的に定義する。例えば、ユーザーログインフォームを FormGroupFormControl で作成でき、login.valuelogin.controlslogin.patchValue などの API を使って操作できる。

以前の Angular では型安全が不十分で、存在しないプロパティにアクセスするコードもコンパイルされてしまった。しかし、厳密に型付けされたリアクティブフォームでは、存在しないプロパティへのアクセスはコンパイル時にエラーになる。これにより IDE でのオートコンプリートやフォーム構造の明示的指定なども可能になり、安全性と開発効率が向上する。これらの改善は現状、リアクティブフォームにのみ適用される。

型なしフォーム

型なしフォームも引き続きサポートされており、従来通り使用できる。使用する場合は、@angular/forms から Untyped シンボルをインポートする必要がある。

const login = new UntypedFormGroup({
    email: new UntypedFormControl(''),
    password: new UntypedFormControl(''),
});

FormControl

最も単純なフォームは、単一のコントロールで構成される。

const email = new FormControl('angularrox@gmail.com');

このコントロールは自動的に FormControl<string | null> 型として推論される。TypeScript は email.valueemail.valueChangesemail.setValue() など、フォームコントロールのすべての API でこの型を自動的に適用する。

ヌラビリティ

コントロールの型に null が含まれるのは、reset() を呼び出すと値が null になる可能性があるため。TypeScript はこれを考慮して型を強制する。もしコントロールを null にしたくない場合は、nonNullable: true オプションを使うと、reset() 時に値が初期値に戻るようになる。ただし、この設定はフォームのランタイム動作に影響するため注意が必要。

const email = new FormControl('angularrox@gmail.com');
email.reset();
console.log(email.value); // null
const email = new FormControl('angularrox@gmail.com', {nonNullable: true});
email.reset();
console.log(email.value); // angularrox@gmail.com

明示的な型の指定

推測に頼るのではなく、型を指定できる。

const email = new FormControl<string|null>(null);
email.setValue('angularrox@gmail.com');

FormArray

FormArray は同じ型のコントロールを動的に管理する配列で、型パラメーターは各内部コントロールの型を示す。例えば、FormArray に文字列コントロールを追加すると、型は FormControl<string | null> になる。

const names = new FormArray([new FormControl('Alex')]);
names.push(new FormControl('Jess'));

もし配列内に異なる型の要素を混在させたい場合は、TypeScript が型を推測できないため、UntypedFormArray を使う必要がある。

FormGroupFormRecord

Angular では、列挙済みのキーを持つフォームには FormGroup 型が使われ、オープンエンドや動的なグループには FormRecord 型が使用される。

部分的な値

const login = new FormGroup({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

FormGroup のコントロールは無効にでき、無効なコントロールは value に含まれない。そのため、login.value の型は Partial<{email: string, password: string}> となり、各プロパティは undefined になる可能性がある。TypeScript は string | undefined として扱い、undefined を考慮するよう強制する。無効なコントロールも含めた値にアクセスしたい場合は login.getRawValue() を使う。

オプションのコントロールと動的グループ

一部のフォームコントロールは、存在する場合と存在しない場合があり、実行時に追加・削除が可能である。こうした可変のコントロールは オプションフィールド として表現できる。

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}
const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});
login.removeControl('password');

このフォームでは、型を明示的に指定しているため、password コントロールをオプションにできる。

FormRecord

キーが事前にわからない場合や動的に追加する場合には、FormRecord を使用する。

const addresses = new FormRecord<FormControl<string|null>>({});
addresses.addControl('Andrew', new FormControl('2340 Folsom St'));

FormRecord には任意の string | null 型のコントロールを追加できる。

FormBuilderNonNullableFormBuilder

FormBuilder は、新しい型をサポートするようにアップグレードされており、さらに NonNullableFormBuilder を使うことで、全てのコントロールに自動で nonNullable: true を適用できる。FormBuildernonNullable プロパティ経由で使用でき、注入時に NonNullableFormBuilder としても扱える。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});
import { NonNullableFormBuilder } from '@angular/forms';

@Component({
    ...
})
export class LoginComponent {
    constructor(private fb: NonNullableFormBuilder) {}
    ...
}

テンプレート駆動フォーム

HTML テンプレートにフォームのロジックを記述する方法で、小規模または単純なフォームに適している。

フォームの構築

まずは、フォームに反映されるフォームモデルを定義する。

export class Actor {
    constructor(
    public id: number,
    public name: string,
    public skill: string,
    public studio?: string,
    ) {}
}

続いてコンポーネントを作成する。

import {Component} from '@angular/core';
import {Actor} from '../actor';
import {FormsModule} from '@angular/forms';
import {JsonPipe} from '@angular/common';
@Component({
    selector: 'app-actor-form',
    templateUrl: './actor-form.component.html',
    imports: [FormsModule, JsonPipe],
})
export class ActorFormComponent {
    skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting'];
    model = new Actor(18, 'Tom Cruise', this.skills[3], 'CW Productions');
    submitted = false;
    onSubmit() {
        this.submitted = true;
    }
...
}

入力コントロールをデータプロパティにバインドする

入力欄を Actor プロパティに双方向バインドするため、FormsModulengModel ディレクティブを使い、名前用 <input>[(ngModel)]="..." を追加してビューとモデルを同期させる。

<input type="text" class="form-control" id="name"
       required
       [(ngModel)]="model.name" name="name">

フォーム全体のステータスにアクセスする

FormsModule をインポートすると、Angular は <form> 要素に自動で ngForm ディレクティブを適用する。フォーム全体や ngForm の状態にアクセスするには、テンプレートでフォーム要素にテンプレート参照変数を付ける。

<form #actorForm="ngForm">

コントロール要素に名前を付ける

[(ngModel)] を使う要素には必ず name 属性が必要で、これは NgForm がフォーム要素を登録するために使われる。例では <input>name="name" を付けてアクター名を表している。同様にスタジオやスキルにも [(ngModel)]name 属性を追加する。

{{ model | json }}
<div class="form-group">
    <label for="name">Name</label>
    <input type="text" class="form-control" id="name"
           required
           [(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
    <label for="studio">Studio</label>
    <input type="text"  class="form-control" id="studio"
           [(ngModel)]="model.studio" name="studio">
</div>
<div class="form-group">
    <label for="skill">Skill</label>
    <select class="form-control"  id="skill"
            required
            [(ngModel)]="model.skill" name="skill">
        @for (skill of skills; track $index) {
            <option [value]="skill">{{ skill }}</option>
        }
    </select>
</div>

コントロールの状態を追跡する

NgModel ディレクティブを追加すると、コントロールの状態に応じて Angular がクラス名を自動付与する。これらのクラスを使えば、状態に応じたスタイル変更が可能になる。

状態 クラス(真の場合) クラス(偽の場合)
コントロールが訪問された ng-touched ng-untouched
コントロールの値が変更された ng-dirty ng-pristine
コントロールの値が有効 ng-valid ng-invalid

状態の視覚的なフィードバックを作成する

ng-validng-invalid クラスは、フォームコントロールの値が有効か無効かを示す。これを使うと、値が無効な場合に視覚的なフィードバックを与えられる。必須フィールドの場合は、左側に色付きバーを表示して、必須であることと入力が無効であることを同時に示すこともできる。

.ng-valid[required], .ng-valid.required  {
    border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form)  {
    border-left: 5px solid #a94442; /* red */
}

検証エラーメッセージの表示と非表示

名前入力ボックスが必須で、クリアすると赤いバーで無効状態を示す。しかし、ユーザーには何が間違っているのか、どう直せばよいのか分からない。この場合、コントロールの状態(例えば ng-invalidng-touched)をチェックして、具体的なエラーメッセージを表示するとユーザーに役立つ。スキル選択ボックスは必須でも、選択肢が制限されているため、追加のエラー表示は不要。

エラーメッセージを適切に定義して表示するには、次の手順を実行する。

入力へのローカル参照を追加

テンプレート参照変数 #name="ngModel" を使うと、テンプレート内から <input> に紐づく Angular の NgModel コントロールにアクセスできる。"ngModel" を指定する理由は、NgModel ディレクティブの exportAs プロパティの値だからで、Angular に対してこの参照変数をディレクティブにリンクする方法を示している。

エラーメッセージの追加

適切なエラーメッセージを含む <div> を追加する。

エラーメッセージを条件付きにする

name コントロールの状態に応じて、<div>hidden プロパティにバインドすることで、エラーメッセージの表示・非表示を制御できる。例えば、コントロールが有効なら非表示にし、無効でかつ触れられている場合に表示する、といった条件を hidden に渡すことで実現可能。

<label for="name">Name</label>
<input type="text" class="form-control" id="name"
       required [(ngModel)]="model.name" name="name"
       #name="ngModel">
<div [hidden]="name.valid || name.pristine"
     class="alert alert-danger">
    Name is required
</div>

ngSubmit でフォームを送信する

フォームに記入後、ユーザーは送信できる。フォーム下部の Submit ボタンは type="submit" が設定されているため、自身では処理を行わないが、クリックするとフォームの送信イベントを発火させる。

ngOnSubmit を監視する

フォームの ngSubmi tイベントプロパティを、アクターフォームコンポーネントの onSubmit() メソッドにバインドする

<form (ngSubmit)="onSubmit()" #actorForm="ngForm">

disabled プロパティをバインドする

テンプレート参照変数 #actorForm を使ってフォームにアクセスし、Submit ボタンにイベントバインディングを設定する。フォーム全体の有効性(actorForm.valid など)を Submit ボタンの disabled プロパティにバインドして、無効なときはボタンを押せないようにする。

<button type="submit" class="btn btn-success"
        [disabled]="!actorForm.form.valid">Submit</button>

フォーム入力の検証

ユーザー入力を検証することでデータの正確性と品質を向上させられる。

テンプレート駆動フォームでの入力検証

テンプレート駆動フォームに検証を追加するには、HTML の検証属性をフォームコントロールに付与する。Angular はこれらを内部のバリデーター関数にマッピングし、値が変更されるたびに検証を実行する。

検証結果は、エラーのリスト(無効な場合)か null(有効な場合)として返される。フォームコントロールの状態は、ngModel をローカルテンプレート変数にエクスポートして確認できる。

<input type="text" id="name" name="name" class="form-control"
       required minlength="4" appForbiddenName="bob"
       [(ngModel)]="actor.name" #name="ngModel">
@if (name.invalid && (name.dirty || name.touched)) {
    <div class="alert">
        @if (name.hasError('required')) {
            <div>
                Name is required.
            </div>
          }
        @if (name.hasError('minlength')) {
            <div>
                Name must be at least 4 characters long.
            </div>
        }
        @if (name.hasError('forbiddenName')) {
            <div>
                Name cannot be Bob.
            </div>
        }
    </div>
}

要素には requiredminlength の HTML 検証属性があり、さらにカスタムバリデーター forbiddenName を追加できる。#name="ngModel" によって、NgModel をローカル変数 name として参照可能になり、validdirty などの状態をテンプレート内で確認できる。外側の *ngIf は、コントロールが無効かつ操作済み(dirty または touched)である場合のみ、ネストされたエラーメッセージを表示する。

リアクティブフォームでの入力検証

リアクティブフォームでは、フォームの正しい値や状態はコンポーネントクラスが管理する。バリデーターもテンプレートではなくクラス内のフォームコントロールに直接設定し、値が変わるたびに Angular が自動で検証する。

バリデーター関数

バリデーター関数は、同期または非同期にすることができる。

バリデーターの種類 詳細
同期バリデーター コントロールを受け取り、検証エラーのセットまたは null を即座に返す関数。FormControl 作成時の第2引数に指定する。
非同期バリデーター コントロールを受け取り、後で検証エラーのセットまたは null を返す Promise または Observable を返す関数。FormControl 作成時の第3引数に指定する。

パフォーマンスのため、Angular はまず同期バリデーターをすべて実行し、すべて合格した場合にのみ非同期バリデーターを実行する。非同期バリデーターは、エラーが設定される前に完了する必要がある。

組み込みバリデーター関数

独自バリデーターを作成することも、組み込みバリデーターを使うこともできる。requiredminlength など、テンプレート駆動フォームで属性として使えるバリデーターは、Validators クラスから関数として使用できる。リアクティブフォームでは、この関数形式を使ってバリデーションを設定する。

アクターフォームをリアクティブフォームに変換する際は、組み込みバリデーターを関数形式で利用してバリデーションを設定する。

actorForm = new FormGroup({
    name: new FormControl(this.actor.name, [
        Validators.required,
        Validators.minLength(4),
        forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator.
    ]),
    role: new FormControl(this.actor.role),
    skill: new FormControl(this.actor.skill, Validators.required),
});

get name() {
    return this.actorForm.get('name');
}
get skill() {
    return this.actorForm.get('skill');
}

この例では、name コントロールに Validators.requiredValidators.minLength(4) の組み込みバリデーターと、カスタムバリデーター forbiddenNameValidator を設定している。これらはすべて同期バリデーターなので、第2引数として配列で渡す。さらに、テンプレートで便利に使えるよう、親フォームグループの get メソッドを省略するゲッターメソッドも追加されている。

カスタムバリデーターの定義

組み込みバリデーターはすべてのユースケースに対応できるわけではないため、必要に応じてカスタムバリデーターを作成することがある。
前の例で使った forbiddenNameValidator 関数の定義を確認する。

/** An actor's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
        const forbidden = nameRe.test(control.value);
        return forbidden ? {forbiddenName: {value: control.value}} : null;
    };
}

forbiddenNameValidator は、禁止する文字列をチェックする正規表現を受け取り、カスタムバリデーター関数を返すファクトリ。関数はフォームコントロールを受け取り、値が有効なら null を、無効なら forbiddenName キーを持つエラーオブジェクトを返す。禁止する名前は「bob」のように設定可能で、他の文字列にも適用できる。非同期バリデーターの場合は Promise か Observable を返し、完了時に最後の値で検証を行う。

リアクティブフォームに追加する

リアクティブフォームでは、FormControl に直接関数を渡すことで、カスタムバリデーターを追加する。

actorForm = new FormGroup({
    name: new FormControl(this.actor.name, [
        Validators.required,
        Validators.minLength(4),
        forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator.
    ]),
    role: new FormControl(this.actor.role),
    skill: new FormControl(this.actor.skill, Validators.required),
});

テンプレート駆動フォームに追加する

テンプレート駆動フォームでは、ディレクティブをテンプレートに追加してバリデーター関数をラップする。

たとえば ForbiddenValidatorDirectiveforbiddenNameValidator をラップする。Angular はディレクティブを NG_VALIDATORS プロバイダーに登録し、フォーム検証プロセスでそのディレクティブを認識する。NG_VALIDATORS は拡張可能なバリデーターのコレクションを持つ定義済みプロバイダー。

@Directive({
    selector: '[appForbiddenName]',
    providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}],
})
export class ForbiddenValidatorDirective implements Validator {
    forbiddenName = input<string>('', {alias: 'appForbiddenName'});
    validate(control: AbstractControl): ValidationErrors | null {
        return this.forbiddenName
            ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control)
            : null;
    }
}

ディレクティブができたら、あとは次の通りに使用する。

<input type="text" id="name" name="name" class="form-control"
       required minlength="4" appForbiddenName="bob"
       [(ngModel)]="actor.name" #name="ngModel">

コントロールステータスCSSクラス

フォームコントロールの状態を反映した CSS クラスが自動的に要素に追加される。これを使うと、フォームの状態に応じて入力欄やボタンのスタイルを動的に変更できる。現在サポートされている主なクラスは以下の通り。

  • .ng-valid
  • .ng-invalid
  • .ng-pending
  • .ng-pristine
  • .ng-dirty
  • .ng-untouched
  • .ng-touched
  • .ng-submitted

この例では、フォームコントロールの状態に応じて境界線の色を変更している。フォームが有効な場合は .ng-valid クラスが適用され、無効な場合は .ng-invalid クラスが適用される。これにより、入力の正誤が視覚的に分かるようになっている。

.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form)  {
    border-left: 5px solid #a94442; /* red */
}
.alert div {
    background-color: #fed3d3;
    color: #820000;
    padding: 1rem;
    margin-bottom: 1rem;
}
.form-group {
    margin-bottom: 1rem;
}
label {
    display: block;
    margin-bottom: .5rem;
}
select {
    width: 100%;
    padding: .5rem;
}

クロスフィールド検証

クロスフィールドバリデーターは、複数フィールドの値を組み合わせて検証するカスタムバリデーター。例えば、互いに矛盾する選択肢を制御したり、アクター名と役割の重複を防いだりできる。

リアクティブフォームに追加する

フォームは、次の構造になっている。

const actorForm = new FormGroup({
    'name': new FormControl(),
    'role': new FormControl(),
    'skill': new FormControl()
});

namerole は兄弟コントロールであるため、両方を1つのカスタムバリデーターで評価するには共通の祖先である FormGroup にバリデーターを追加する必要がある。FormGroup 内で子コントロールを取得して値を比較し、作成時に第2引数でバリデーターを渡すことで設定できる。

const actorForm = new FormGroup({
    'name': new FormControl(),
    'role': new FormControl(),
    'skill': new FormControl()
}, { validators: unambiguousRoleValidator });

バリデーターのコードは次のとおり。

/** An actor's name can't match the actor's role */
export const unambiguousRoleValidator: ValidatorFn = (
    control: AbstractControl,
): ValidationErrors | null => {
    const name = control.get('name');
    const role = control.get('role');
    return name && role && name.value === role.value ? {unambiguousRole: true} : null;
};

unambiguousRoleValidatorValidatorFn を実装しており、FormGroup を受け取り、フォームが有効なら null、無効なら ValidationErrors を返す。FormGroupget メソッドで namerole の子コントロールを取得し、値を比較する。値が一致しなければバリデーターは null を返し、値が一致すればエラーオブジェクトを返してフォームを無効にする。

テンプレート駆動フォームに追加する

テンプレート駆動フォームの場合、バリデーター関数をラップするディレクティブを作成する必要がある。

@Directive({
    selector: '[appUnambiguousRole]',
    providers: [
        {provide: NG_VALIDATORS, useExisting: UnambiguousRoleValidatorDirective, multi: true},
    ],
})
export class UnambiguousRoleValidatorDirective implements Validator {
    validate(control: AbstractControl): ValidationErrors | null {
        return unambiguousRoleValidator(control);
    }
}

非同期バリデーターの作成

非同期バリデーターは AsyncValidatorFn または AsyncValidator を実装し、同期バリデーターと似ているがいくつか異なる。validate() 関数は Promise か有限のオブザーバブルを返す必要があり、同期検証が成功した場合にのみ実行される。非同期検証中、フォームコントロールは pending 状態になり、この状態を利用して UI にスピナーなどの進行状況を表示できる。

<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
@if(model.pending) {
    <app-spinner />
}

カスタム非同期バリデーターの実装

この例では、非同期バリデーターは、アクターが既に割り当てられている役割を選ばないようにするために使われる。新しいアクターは常にオーディション中で、既存のアクターは引退しているため、事前に利用可能な役割のリストを取得できない。そのため、バリデーターは現在のキャスト情報を中央データベースから非同期で取得し、入力を検証する。

このコードは、AsyncValidator インターフェースを実装した UniqueRoleValidator クラスを定義している。フォームコントロールの値を非同期で検証し、すでに割り当てられている役割と重複していないかチェックするバリデーターとして機能する。

@Injectable({providedIn: 'root'})
export class UniqueRoleValidator implements AsyncValidator {
    private readonly actorsService = inject(ActorsService);
    validate(control: AbstractControl): Observable<ValidationErrors | null> {
        return this.actorsService.isRoleTaken(control.value).pipe(
            map((isTaken) => (isTaken ? {uniqueRole: true} : null)),
            catchError(() => of(null)),
        );
    }
}

実際のアプリでは、ActorsService が HTTP リクエストで役割の利用状況をチェックする。バリデーターはサービスの実装には依存せず、インターフェースだけを使えばよい。
UnambiguousRoleValidator は、現在のコントロール値を ActorsService.isRoleTaken() に渡して検証を行う。検証中はコントロールが pending 状態になり、validate() が返す Observable が完了するまでその状態が続く。
isRoleTaken()Observable<boolean> を返し、validate()map 演算子で結果を検証結果に変換する。フォームが有効なら null、無効なら ValidationErrors を返す。catchError を使って、HTTP リクエストが失敗した場合でもエラーを無視して検証を続行することができる。
Observable チェーンが完了すると pending は false になり、フォームの有効性が更新される。

リアクティブフォームに追加する

リアクティブフォームで非同期バリデーターを使用するには、最初にバリデーターをコンポーネントクラスのプロパティに注入する。

roleValidator = inject(UniqueRoleValidator);

次に、バリデーター関数を FormControl に直接渡して、適用する。

この例では、UnambiguousRoleValidatorvalidate 関数を roleControl に適用している。asyncValidators オプションに渡すことで、このコントロールは非同期バリデーションを行うようになる。validate 関数はクラスメソッドなので、bind(this.roleValidator) を使って UnambiguousRoleValidator インスタンスに正しくバインドしている。asyncValidators には単一の関数でも関数の配列でも指定可能で、コントロールの非同期バリデーションを柔軟に設定できる。updateOn: 'blur' により、ユーザーが入力フィールドからフォーカスを離したタイミングで非同期バリデーションが実行される。

const roleControl = new FormControl('', {
    asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)],
    updateOn: 'blur',
});

テンプレート駆動フォームに追加する

テンプレート駆動フォームで非同期バリデーターを使うには、まずディレクティブを作る必要がある。ディレクティブには NG_ASYNC_VALIDATORS プロバイダーを登録し、Angular が非同期検証を認識できるようにする。

ディレクティブ内では、UniqueRoleValidator クラスを注入し、validate 関数で実際の検証ロジックを呼び出す。Angular はコントロールの値が変化したときや検証が必要になったときに、この validate 関数を自動的に実行する。

@Directive({
    selector: '[appUniqueRole]',
    providers: [
        {
            provide: NG_ASYNC_VALIDATORS,
            useExisting: forwardRef(() => UniqueRoleValidatorDirective),
            multi: true,
        },
    ],
})
export class UniqueRoleValidatorDirective implements AsyncValidator {
    private readonly validator = inject(UniqueRoleValidator);
    validate(control: AbstractControl): Observable<ValidationErrors | null> {
        return this.validator.validate(control);
    }
}
<input type="text"
       id="role"
       name="role"
       #role="ngModel"
       [(ngModel)]="actor.role"
       [ngModelOptions]="{ updateOn: 'blur' }"
       appUniqueRole>

パフォーマンスの最適化

フォームでは通常、値が変わるたびにバリデーターが実行される。同期バリデーターは軽量で影響がほとんどないが、非同期バリデーターは HTTP リクエストなどを伴うため、毎回実行するとバックエンドに負荷がかかる。

この問題を避けるには、updateOn プロパティを change(デフォルト)から blursubmit に変更して、バリデーションのタイミングを遅らせる。テンプレート駆動フォームの場合は、テンプレート内でこのプロパティを設定する。

テンプレート駆動フォームでは、テンプレートでプロパティの設定をする。

<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">

リアクティブフォームでは、FormControl インスタンスでプロパティを設定する。

new FormControl('', {updateOn: 'blur'});

動的フォームの構築

アンケートなど似た形式のフォームは、メタデータをもとに動的フォームテンプレートを作成することで効率的に生成できる。チュートリアルでは、ヒーロー雇用申請フォームを例に、リアクティブフォームでデータモデルを作成・動的にフォームコントロールを生成し、入力検証やスタイリングで有効な場合のみ送信可能にする基本的な動的フォームを構築する。

フォームオブジェクトモデルを作成する

動的フォームでは、フォームのあらゆるシナリオを表現できるオブジェクトモデルが必要。ヒーロー申請フォームでは各フォームコントロールが質問と回答を担当する。これを表現するために、DynamicFormQuestionComponent やベースクラス QuestionBase が用意されており、フォーム内の質問と回答をモデル化する。

question-base.ts
export class QuestionBase<T> {
    value: T | undefined;
    key: string;
    label: string;
    required: boolean;
    order: number;
    controlType: string;
    type: string;
    options: {key: string; value: string}[];
    constructor(
        options: {
            value?: T;
            key?: string;
            label?: string;
            required?: boolean;
            order?: number;
            controlType?: string;
            type?: string;
            options?: {key: string; value: string}[];
        } = {},
    ) {
        this.value = options.value;
        this.key = options.key || '';
        this.label = options.label || '';
        this.required = !!options.required;
        this.order = options.order === undefined ? 1 : options.order;
        this.controlType = options.controlType || '';
        this.type = options.type || '';
        this.options = options.options || [];
    }
}

コントロールクラスを定義する

ベースクラスから TextboxQuestionDropdownQuestion の2つを派生させる。フォームテンプレートでコントロールを動的にレンダリングするときは、これらの質問タイプのインスタンスを使う。

TextboxQuestion は input 要素として表示され、質問を提示してユーザーが入力できる。入力タイプは optionstype フィールド(例: text、email、url)に従って設定する。

question-textbox.ts
import {QuestionBase} from './question-base';
export class TextboxQuestion extends QuestionBase<string> {
    override controlType = 'textbox';
}

DropdownQuestion 選択ボックスに選択肢のリストを表示する。

question-dropdown.ts
import {QuestionBase} from './question-base';
export class DropdownQuestion extends QuestionBase<string> {
    override controlType = 'dropdown';
}

フォームグループを構成する

動的フォームは、サービスを使って質問モデルのメタデータから入力コントロールのセットを作成する。QuestionControlService は質問モデルを受け取り、デフォルト値や検証ルールを含む FormGroup のセットを生成する。

question-control.service.ts
import {Injectable} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {QuestionBase} from './question-base';
@Injectable()
export class QuestionControlService {
    toFormGroup(questions: QuestionBase<string>[]) {
        const group: any = {};
        questions.forEach((question) => {
            group[question.key] = question.required
                ? new FormControl(question.value || '', Validators.required)
                : new FormControl(question.value || '');
    });
    return new FormGroup(group);
    }
}

any 型を使っているため、個人的には型安全に思えない。

動的フォームコンテンツを構成する

動的フォームはコンテナーコンポーネントで表され、各質問は <app-question> タグで DynamicFormQuestionComponent のインスタンスとして配置される。

DynamicFormQuestionComponent は、質問オブジェクトの値に基づいて個々の質問をレンダリングし、formGroup ディレクティブを使ってテンプレートとコントロールを接続する。質問モデルに従ってフォームグループを作成し、各コントロールに表示や検証ルールを設定する。

<div [formGroup]="form()">
    <label [attr.for]="question().key">{{ question().label }}</label>
    <div>
        @switch (question().controlType) {
            @case ('textbox') {
                <input [formControlName]="question().key" [id]="question().key" [type]="question().type" />
            }
            @case ('dropdown') {
                <select [id]="question().key" [formControlName]="question().key">
                    @for (opt of question().options; track opt) {
                        <option [value]="opt.key">{{ opt.value }}</option>
                    }
                </select>
            }
        }
    </div>
    @if (!isValid) {
        <div class="errorMessage">{{ question().label }} is required</div>
    }
</div>

DynamicFormQuestionComponent の目的は、モデルで定義された質問タイプを表示すること。現在は質問タイプが2種類だが、将来的に追加も可能。テンプレート内の @switch ブロックで質問タイプを判定し、formControlNameformGroup ディレクティブを使って適切な入力コントロールをレンダリングする。これらのディレクティブは ReactiveFormsModule で提供される。

データを供給する

個々のフォームを生成するには、質問セットを提供するサービスが必要。ここでは、ハードコードされたサンプルデータを返す QuestionService を作成。実際のアプリではバックエンドから取得することも可能だが、重要なのはサービスから返されるオブジェクトで質問を完全に制御できる点。要件変更時は questions 配列にオブジェクトを追加・更新・削除するだけでフォーム内容を調整できる。

question.service.ts
import {Injectable} from '@angular/core';
import {DropdownQuestion} from './question-dropdown';
import {QuestionBase} from './question-base';
import {TextboxQuestion} from './question-textbox';
import {of} from 'rxjs';
@Injectable()
export class QuestionService {
    // TODO: get from a remote source of question metadata
    getQuestions() {
        const questions: QuestionBase<string>[] = [
            new DropdownQuestion({
                key: 'favoriteAnimal',
                label: 'Favorite Animal',
                options: [
                    {key: 'cat', value: 'Cat'},
                    {key: 'dog', value: 'Dog'},
                    {key: 'horse', value: 'Horse'},
                    {key: 'capybara', value: 'Capybara'},
                ],
                order: 3,
            }),
            new TextboxQuestion({
                key: 'firstName',
                label: 'First name',
                value: 'Alex',
                required: true,
                order: 1,
            }),
            new TextboxQuestion({
                key: 'emailAddress',
                label: 'Email',
                type: 'email',
                order: 2,
            }),
        ];
        return of(questions.sort((a, b) => a.order - b.order));
    }
}

動的フォームテンプレートを作成する

DynamicFormComponent<app-dynamic-form> で表されるフォームのエントリポイントで、メインコンテナーとして機能する。各質問は <app-question> 要素にバインドされ、DynamicFormQuestionComponent と対応する形で表示される。

<div>
    <form (ngSubmit)="onSubmit()" [formGroup]="form()">
        @for (question of questions(); track question) {
            <div class="form-row">
                <app-question [question]="question" [form]="form()" />
            </div>
        }
        <div class="form-row">
            <button type="submit" [disabled]="!form().valid">Save</button>
        </div>
    </form>
    @if (payLoad) {
        <div class="form-row"><strong>Saved the following values</strong><br />{{ payLoad }}</div>
    }
</div>
import {Component, computed, inject, input} from '@angular/core';
import {FormGroup, ReactiveFormsModule} from '@angular/forms';
import {DynamicFormQuestionComponent} from './dynamic-form-question.component';
import {QuestionBase} from './question-base';
import {QuestionControlService} from './question-control.service';
@Component({
    selector: 'app-dynamic-form',
    templateUrl: './dynamic-form.component.html',
    providers: [QuestionControlService],
    imports: [DynamicFormQuestionComponent, ReactiveFormsModule],
})
export class DynamicFormComponent {
    private readonly qcs = inject(QuestionControlService);
    questions = input<QuestionBase<string>[] | null>([]);
    form = computed<FormGroup>(() =>
        this.qcs.toFormGroup(this.questions() as QuestionBase<string>[]),
    );
    payLoad = '';
    onSubmit() {
        this.payLoad = JSON.stringify(this.form().getRawValue());
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?