0
1

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技術要素解説】フォームの作り方を理解する:Template-drivenからSignal Formsまで

0
Posted at

はじめに

Angular技術要素解説シリーズ、第7弾です。

今回は、ユーザー入力を扱うフォームの作り方を解説します。

Angularには「Template-driven Forms」「Reactive Forms」という2つの方式があり、さらにAngular 22で「Signal Forms」が安定版として加わりました。

それぞれの違いを整理します。

この記事で扱う技術要素

  • Template-driven Forms(テンプレート駆動フォーム)
  • Reactive Forms(リアクティブフォーム)
  • FormControl / FormGroup / FormArray
  • バリデーション(Validators)
  • Signal Forms

Template-driven Forms(テンプレート駆動フォーム)

シンプルなフォームに向いている方式で、以前解説した双方向バインディング([(ngModel)])を軸に構築します。

import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
      <label>
        お名前:
        <input name="name" [(ngModel)]="formData.name" required minlength="2">
      </label>

      <label>
        メールアドレス:
        <input name="email" [(ngModel)]="formData.email" required email>
      </label>

      <button type="submit" [disabled]="contactForm.invalid">送信</button>
    </form>

    @if (contactForm.submitted) {
      <p>送信内容: {{ formData.name }} 様 ({{ formData.email }})</p>
    }
  `,
})
export class ContactFormComponent {
  formData = {
    name: '',
    email: '',
  };

  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('送信されたデータ:', this.formData);
    }
  }
}

required・minlength・emailといった、通常のHTML標準属性のような書き方でバリデーションルールを指定できるのが特徴です。

#contactForm="ngForm"のようにテンプレート参照変数を使うことで、フォーム全体の状態(invalid・submittedなど)をテンプレート内から直接参照できます。

手軽に書ける反面、複雑な条件分岐を伴うバリデーションや、動的にフィールドを増減させるような高度な制御はやや苦手です。

Reactive Forms(リアクティブフォーム)

大規模・複雑なフォームに向いている方式で、フォームの構造をTypeScript側で明示的に定義します。

FormControl:単一の入力項目

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  selector: 'app-name-field',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="nameControl">

    @if (nameControl.invalid && nameControl.touched) {
      <p class="error">2文字以上で入力してください</p>
    }
  `,
})
export class NameFieldComponent {
  nameControl = new FormControl('', [Validators.required, Validators.minLength(2)]);
}

FormGroup:複数の項目をまとめる

import { Component } from '@angular/core';
import { FormGroup, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  selector: 'app-signup-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
      <label>
        お名前:
        <input formControlName="name">
        @if (signupForm.get('name')?.invalid && signupForm.get('name')?.touched) {
          <span class="error">お名前は必須です</span>
        }
      </label>

      <label>
        パスワード:
        <input type="password" formControlName="password">
        @if (signupForm.get('password')?.hasError('minlength')) {
          <span class="error">8文字以上で入力してください</span>
        }
      </label>

      <button type="submit" [disabled]="signupForm.invalid">登録</button>
    </form>
  `,
})
export class SignupFormComponent {
  signupForm = new FormGroup({
    name: new FormControl('', [Validators.required]),
    password: new FormControl('', [Validators.required, Validators.minLength(8)]),
  });

  onSubmit() {
    if (this.signupForm.valid) {
      console.log('登録データ:', this.signupForm.value);
      // { name: "...", password: "..." }
    }
  }
}

Reactive Formsでは、フォームの構造(どんな項目があり、どんなバリデーションが付いているか)がTypeScript側の1箇所に明示的に定義されるため、複雑なフォームでも見通しが良く、単体テストも書きやすくなります。

FormArray:可変長の項目群

「電話番号を複数登録できる」ような、項目数が動的に増減するフォームにはFormArrayを使います。

import { Component } from '@angular/core';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  selector: 'app-phone-list-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form">
      <div formArrayName="phoneNumbers">
        @for (control of phoneNumbers.controls; track $index) {
          <div>
            <input [formControlName]="$index" placeholder="電話番号">
            <button type="button" (click)="removePhoneNumber($index)">削除</button>
          </div>
        }
      </div>
      <button type="button" (click)="addPhoneNumber()">電話番号を追加</button>
    </form>
  `,
})
export class PhoneListFormComponent {
  form = new FormGroup({
    phoneNumbers: new FormArray([
      new FormControl('', Validators.required),
    ]),
  });

  get phoneNumbers(): FormArray {
    return this.form.get('phoneNumbers') as FormArray;
  }

  addPhoneNumber() {
    this.phoneNumbers.push(new FormControl('', Validators.required));
  }

  removePhoneNumber(index: number) {
    this.phoneNumbers.removeAt(index);
  }
}

「電話番号を追加」ボタンを押すたびに、FormArrayの中に新しいFormControlが追加され、テンプレート側もそれに追随して入力欄が増えていきます。

カスタムバリデーターの作成

組み込みのValidatorsだけでは足りない、独自のバリデーションルールも作成できます。

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// 「英数字が両方含まれているか」をチェックする独自バリデーター
export function alphanumericValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value as string;
    if (!value) return null;

    const hasLetter = /[a-zA-Z]/.test(value);
    const hasNumber = /[0-9]/.test(value);

    return hasLetter && hasNumber ? null : { alphanumeric: true };
  };
}
password = new FormControl('', [Validators.required, Validators.minLength(8), alphanumericValidator()]);

Signal Forms(Angular 22で安定版)

Angular 22で、フォームの状態をSignalsで扱うSignal Formsが安定版になりました。

基本的な考え方はReactive Formsに近いですが、内部の状態管理がSignalベースになっている点が異なります。

import { Component } from '@angular/core';
import { form, Field } from '@angular/forms/signals'; // イメージを伝えるための概念的なAPI例

@Component({
  selector: 'app-signal-form-example',
  standalone: true,
  imports: [Field],
  template: `
    <input [field]="signupForm.name" />
    @if (signupForm.name().invalid()) {
      <span class="error">お名前を入力してください</span>
    }
  `,
})
export class SignalFormExampleComponent {
  signupForm = form({
    name: '',
  });
}

※Signal Formsは登場したばかりのAPIのため、具体的な書き方は今後変わる可能性があります。

詳細は必ずAngular公式ドキュメントの最新情報を確認してください。

Signal Formsの狙いは、フォームの状態変化を、これまでのvalueChanges(Observableベース)ではなく、アプリ全体で統一されたSignalベースの仕組みに揃えることにあります。

3方式の比較

観点 Template-driven Forms Reactive Forms Signal Forms
フォーム構造の定義場所 テンプレート(HTML)側が中心 TypeScript側で明示的に定義 TypeScript側でSignalベースに定義
向いている規模 シンプルなフォーム 複雑・大規模なフォーム 今後主流になる可能性がある新方式
テストのしやすさ やや低い(DOM操作が絡みやすい) 高い(クラス単体でテスト可能) 高い(想定)
動的な項目の増減 苦手 FormArrayで対応可能 今後のAPI拡充次第
状態管理の内部実装 Observable(RxJS)ベース Observable(RxJS)ベース Signalベース

実務では、簡単な検索ボックスなどはTemplate-driven Formsで手早く、会員登録フォームやアンケートフォームのような複雑なものはReactive Formsで、というように使い分けられることが多いです。

Signal Formsは2026年時点でまだ登場したばかりの機能であり、今後の主要な選択肢になっていく可能性があります。

まとめ

用語 役割
Template-driven Forms [(ngModel)]を中心にHTML側でフォームを組み立てる、シンプルな方式
Reactive Forms TypeScript側でフォーム構造を明示的に定義する、堅牢な方式
FormControl 単一の入力項目を表すクラス
FormGroup 複数のFormControlをまとめるクラス
FormArray 可変長のFormControl群を扱うクラス
Signal Forms Angular 22で安定版となった、Signalベースの新しいフォーム管理方式

次回は、Angularアプリを本番環境に届けるためのビルドツールとパフォーマンス最適化について解説します。

学習に使った書籍

本記事の内容整理にあたっても、以下の書籍を参考にしました。

※上記リンクはアフィリエイトリンクを含みます。

参考

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?