Reactive Forms(リアクティブフォーム)
| 対応: | Angular 14(2022) |
|---|
『Angular』のリアクティブフォームは、TypeScript コードでフォームの構造・初期値・バリデーションをすべて定義する方式です。テンプレート駆動フォームとは異なり、フォームの状態を FormControl / FormGroup / FormArray というオブジェクトで表現し、コンポーネントクラス側で一元管理します。テストのしやすさ・複雑なバリデーションへの対応・動的フィールドの追加といった場面で特に威力を発揮します。
基本構文
// ReactiveFormsModule を imports に追加してリアクティブフォームを有効にします
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-example',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<!-- [formGroup] でコンポーネントの formGroup インスタンスとテンプレートを紐づけます -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- formControlName で FormGroup 内のコントロール名を指定します -->
<input formControlName="email" type="email" />
<button type="submit">送信</button>
</form>
`,
})
export class ExampleComponent {
// FormGroup でフォーム全体を定義します
form = new FormGroup({
// FormControl の第1引数は初期値、第2引数にバリデーターを渡します
email: new FormControl('', [Validators.required, Validators.email]),
});
onSubmit(): void {
console.log(this.form.value);
}
}
主なクラス・プロパティ一覧
| クラス / プロパティ | 概要 |
|---|---|
FormControl | 単一の入力フィールドを表すクラスです。初期値・バリデーターを引数で受け取ります。 |
FormGroup | 複数の FormControl をまとめてひとつのオブジェクトとして管理するクラスです。フォーム全体や関連するフィールドのグループに使います。 |
FormArray | 同じ種類のコントロールを配列として動的に増減させるクラスです。繰り返し入力欄(メールアドレスを複数追加するなど)に使います。 |
FormBuilder | FormGroup / FormControl の生成を簡潔に書けるサービスです。inject(FormBuilder) または DI で取得して使います。 |
.value | フォーム全体または個別コントロールの現在の値をオブジェクトとして取得するプロパティです。 |
.valid / .invalid | バリデーションの合否を boolean で返すプロパティです。 |
.errors | バリデーションエラーの詳細をオブジェクトで返すプロパティです。エラーがない場合は null になります。 |
.touched | ユーザーがそのフィールドをフォーカスして離れた場合に true になるプロパティです。エラーメッセージの表示タイミング制御によく使います。 |
.valueChanges | 値が変化するたびに通知される Observable です。リアルタイムバリデーションや連動入力欄の実装に使います。 |
主な組み込みバリデーター一覧
| バリデーター | 概要 |
|---|---|
Validators.required | 値が空でないことを必須とするバリデーターです。 |
Validators.minLength(n) | 文字列の最小文字数を n 文字に設定するバリデーターです。 |
Validators.maxLength(n) | 文字列の最大文字数を n 文字に設定するバリデーターです。 |
Validators.min(n) | 数値の最小値を n に設定するバリデーターです。 |
Validators.max(n) | 数値の最大値を n に設定するバリデーターです。 |
Validators.email | メールアドレスの形式かどうかを検証するバリデーターです。 |
Validators.pattern(regexp) | 正規表現にマッチするかどうかを検証するバリデーターです。 |
サンプルコード
ユーザー登録フォームでメールアドレスとパスワードを FormGroup で管理し、エラーメッセージを表示する例です。
// register.component.ts
// メールアドレスとパスワードを入力するユーザー登録フォームです
// バリデーションエラーはフィールドにタッチした後にのみ表示します
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-register',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<!-- メールアドレス入力欄 -->
<div>
<label for="email">メールアドレス</label>
<input id="email" type="email" formControlName="email" />
<!-- touched かつ invalid のときだけエラーを表示します -->
<div *ngIf="email.touched && email.invalid">
<span *ngIf="email.errors?.['required']">メールアドレスは必須です。</span>
<span *ngIf="email.errors?.['email']">正しいメールアドレスを入力してください。</span>
</div>
</div>
<!-- パスワード入力欄 -->
<div>
<label for="password">パスワード</label>
<input id="password" type="password" formControlName="password" />
<div *ngIf="password.touched && password.invalid">
<span *ngIf="password.errors?.['required']">パスワードは必須です。</span>
<span *ngIf="password.errors?.['minlength']">8文字以上で入力してください。</span>
</div>
</div>
<!-- フォーム全体が valid のときだけ送信ボタンを有効にします -->
<button type="submit" [disabled]="registerForm.invalid">登録する</button>
</form>
`,
})
export class RegisterComponent {
// FormGroup でメールとパスワードをまとめて管理します
registerForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
// minLength(8) でパスワードの最小文字数を8文字に設定します
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
});
// テンプレートから個別コントロールにアクセスしやすいようにゲッターを定義します
get email(): FormControl {
return this.registerForm.get('email') as FormControl;
}
get password(): FormControl {
return this.registerForm.get('password') as FormControl;
}
onSubmit(): void {
if (this.registerForm.valid) {
console.log('送信データ:', this.registerForm.value);
}
}
}
FormBuilder を使って同じフォームをより簡潔に書く例です。
// register-fb.component.ts
// FormBuilder を使うと new FormControl() を省略して簡潔に書けます
// inject() 関数で FormBuilder を取得するモダンな書き方です
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-register-fb',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" placeholder="メールアドレス" />
<input formControlName="password" type="password" placeholder="パスワード" />
<button type="submit" [disabled]="form.invalid">登録する</button>
</form>
`,
})
export class RegisterFbComponent {
// inject() で FormBuilder サービスを取得します
private fb = inject(FormBuilder);
// fb.group() を使うと [初期値, バリデーター] の配列形式で簡潔に定義できます
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
onSubmit(): void {
if (this.form.valid) {
console.log('送信データ:', this.form.value);
}
}
}
カスタムバリデーターを作成してパスワードの一致確認を行う例です。
// password-match.component.ts
// パスワードと確認用パスワードが一致するかをカスタムバリデーターで検証します
// カスタムバリデーターは AbstractControl を受け取り ValidationErrors | null を返す関数です
import { Component, inject } from '@angular/core';
import {
ReactiveFormsModule,
FormBuilder,
Validators,
AbstractControl,
ValidationErrors,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
// カスタムバリデーター関数:パスワードと確認用パスワードが一致しているか検証します
// FormGroup レベルで設定することで2つのフィールドを同時に参照できます
function passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
// 不一致の場合は { passwordMismatch: true } を返してエラーとして扱います
return password === confirm ? null : { passwordMismatch: true };
}
@Component({
selector: 'app-password-match',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="password" type="password" placeholder="パスワード" />
<input formControlName="confirmPassword" type="password" placeholder="パスワード(確認)" />
<!-- FormGroup レベルのエラーは form.errors で取得します -->
<div *ngIf="form.errors?.['passwordMismatch'] && form.get('confirmPassword')?.touched">
パスワードが一致しません。
</div>
<button type="submit" [disabled]="form.invalid">登録する</button>
</form>
`,
})
export class PasswordMatchComponent {
private fb = inject(FormBuilder);
// validators オプションに FormGroup レベルのカスタムバリデーターを設定します
form = this.fb.group(
{
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
},
{ validators: passwordMatchValidator }
);
onSubmit(): void {
if (this.form.valid) {
console.log('送信データ:', this.form.value);
}
}
}
FormArray を使ってメールアドレスを動的に追加・削除する例です。
// email-list.component.ts
// メールアドレスを任意の件数追加・削除できるフォームです
// FormArray でコントロールを配列として動的に管理します
import { Component, inject } from '@angular/core';
import {
ReactiveFormsModule,
FormBuilder,
FormArray,
FormControl,
Validators,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-email-list',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- formArrayName で FormArray のキー名を指定します -->
<div formArrayName="emails">
<!-- FormArray の各コントロールにインデックスで formControlName を割り当てます -->
<div *ngFor="let ctrl of emails.controls; let i = index">
<input [formControlName]="i" type="email" placeholder="メールアドレス" />
<!-- 2件目以降は削除ボタンを表示します -->
<button type="button" *ngIf="i > 0" (click)="removeEmail(i)">削除</button>
</div>
</div>
<!-- ボタンを押すと入力欄を1件追加します -->
<button type="button" (click)="addEmail()">メールアドレスを追加</button>
<button type="submit" [disabled]="form.invalid">送信</button>
</form>
`,
})
export class EmailListComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
// FormArray の初期値として空のメールアドレス入力欄を1件設定します
emails: this.fb.array([
new FormControl('', [Validators.required, Validators.email]),
]),
});
// テンプレートから FormArray に型安全にアクセスするゲッターです
get emails(): FormArray {
return this.form.get('emails') as FormArray;
}
addEmail(): void {
// push() でコントロールを末尾に追加します
this.emails.push(new FormControl('', [Validators.required, Validators.email]));
}
removeEmail(index: number): void {
// removeAt() で指定インデックスのコントロールを削除します
this.emails.removeAt(index);
}
onSubmit(): void {
if (this.form.valid) {
console.log('メールリスト:', this.form.value);
}
}
}
注意点
| 項目 | 説明 |
|---|---|
ReactiveFormsModule のインポート | スタンドアロンコンポーネントでは imports 配列に ReactiveFormsModule を追加しないと [formGroup] などのディレクティブが動作しません。 |
| テンプレート駆動フォームとの併用禁止 | ngModel と formControlName を同じコントロールに混在させることはできません。どちらか一方の方式で統一してください。 |
get() の戻り値の型 | formGroup.get('key') は AbstractControl | null を返します。型安全にアクセスするには as FormControl でキャストするか、Angular 14以降の FormGroup<T> の型パラメーターを活用してください。 |
| 非同期バリデーター | サーバーへの問い合わせが必要なバリデーション(メールアドレスの重複確認など)は AsyncValidatorFn として第3引数に渡します。Observable または Promise を返す関数として実装します。 |
updateOn オプション | デフォルトは値変化のたびにバリデーションが走ります。{ updateOn: 'blur' } や { updateOn: 'submit' } を指定するとバリデーションのタイミングを変更できます。 |
概要
リアクティブフォームは『Angular』でフォームを扱うふたつの方式のうち、より高度なコントロールが可能な方式です。FormControl / FormGroup / FormArray の3つのクラスを組み合わせることで、シンプルなログインフォームから動的なフィールド追加・複雑なクロスフィールドバリデーションまで対応できます。FormBuilder を利用すると定義が簡潔になり、カスタムバリデーター関数を組み合わせることで独自の検証ロジックも柔軟に追加できます。
フォームのバリデーション結果はクラス側で完結するためユニットテストが容易であり、規模が大きくなるほどテンプレート駆動フォームよりも保守性が高まります。動的なフィールドが不要な小規模フォームであればテンプレート駆動フォームも選択肢に入りますが、業務用アプリケーションや入力項目が多い場面ではリアクティブフォームが選択されることが多いです。
フォームで入力値を双方向バインディングで扱う方法については ngModel もご覧ください。サービスを介したデータの受け渡し方法については サービス も参照してください。
よくあるミス: ReactiveFormsModule を imports に追加し忘れる
standalone コンポーネントで [formGroup] や formControlName などのディレクティブを使うには、@Component の imports 配列に ReactiveFormsModule を追加する必要があります。追加し忘れると「Can't bind to 'formGroup'」というエラーになります。
NG
// login.component.ts
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
// ReactiveFormsModule を imports に追加していないためエラーになります
imports: [],
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<button type="submit">ログイン</button>
</form>
`,
})
export class LoginComponent {
loginForm = new FormGroup({
email: new FormControl('vegeta@capsule-corp.jp', [Validators.required, Validators.email]),
});
onSubmit(): void {
console.log(this.loginForm.value);
}
}
OK
// login.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
// ReactiveFormsModule を imports に追加します
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<button type="submit">ログイン</button>
</form>
`,
})
export class LoginComponent {
loginForm = new FormGroup({
email: new FormControl('vegeta@capsule-corp.jp', [Validators.required, Validators.email]),
});
onSubmit(): void {
console.log(this.loginForm.value);
}
}
よくあるミス: ngModel と formControlName を同じフィールドに混在させる
リアクティブフォームの formControlName と、テンプレート駆動フォームの ngModel を同じ入力フィールドに同時に使うことはできません。混在させると実行時エラーになります。どちらか一方の方式で統一します。
NG
// contact.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-contact',
standalone: true,
imports: [ReactiveFormsModule, FormsModule],
template: `
<form [formGroup]="contactForm">
<!-- formControlName と ngModel を同時に使っているためエラーになります -->
<input formControlName="name" [(ngModel)]="name" />
</form>
`,
})
export class ContactComponent {
name = 'Son Goku';
contactForm = new FormGroup({
name: new FormControl('Son Goku'),
});
}
OK
// contact.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'app-contact',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="contactForm" (ngSubmit)="onSubmit()">
<!-- リアクティブフォームでは formControlName のみを使います -->
<input formControlName="name" />
<input formControlName="email" type="email" />
<button type="submit">送信</button>
</form>
`,
})
export class ContactComponent {
contactForm = new FormGroup({
name: new FormControl('Son Goku'),
email: new FormControl('goku@capsule-corp.jp'),
});
onSubmit(): void {
console.log(this.contactForm.value);
}
}
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。