言語
日本語
English

Caution

お使いのブラウザはJavaScriptが無効になっております。
当サイトでは検索などの処理にJavaScriptを使用しています。
より快適にご利用頂くため、JavaScriptを有効にしたうえで当サイトを閲覧することをお勧めいたします。

  1. トップページ
  2. Angular辞典
  3. Reactive Forms(リアクティブフォーム)

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同じ種類のコントロールを配列として動的に増減させるクラスです。繰り返し入力欄(メールアドレスを複数追加するなど)に使います。
FormBuilderFormGroup / 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] などのディレクティブが動作しません。
テンプレート駆動フォームとの併用禁止ngModelformControlName を同じコントロールに混在させることはできません。どちらか一方の方式で統一してください。
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 などのディレクティブを使うには、@Componentimports 配列に 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);
  }
}

記事の間違いや著作権の侵害等ございましたらお手数ですがまでご連絡頂ければ幸いです。