詳細ガイド
フォーム

フォームモデル

フォームモデルはシグナルフォームの基盤であり、フォームデータのための単一の信頼できる情報源として機能します。このガイドでは、フォームモデルの作成方法、更新方法、そして保守性のための設計方法について説明します。

NOTE: フォームモデルは、コンポーネントの双方向バインディングに使用されるAngularのmodel()シグナルとは異なります。フォームモデルはフォームデータを格納する書き込み可能なシグナルであるのに対し、model()は親子コンポーネント間の通信のための入力/出力を作成します。

フォームモデルが解決すること

フォームでは、時間とともに変化するデータを管理する必要があります。明確な構造がないと、このデータはコンポーネントのプロパティ全体に散らばってしまい、変更の追跡、入力の検証、サーバーへのデータ送信が困難になります。

フォームモデルは、フォームデータを単一の書き込み可能なシグナルに集約することで、この問題を解決します。モデルが更新されると、フォームは自動的にその変更を反映します。ユーザーがフォームを操作すると、モデルもそれに応じて更新されます。

モデルの作成

フォームモデルは、Angularのsignal()関数で作成される書き込み可能なシグナルです。このシグナルは、フォームのデータ構造を表すオブジェクトを保持します。

import {Component, signal} from '@angular/core';
import {form, FormField} from '@angular/forms/signals';

@Component({
  selector: 'app-login',
  imports: [FormField],
  template: `
    <input type="email" [formField]="loginForm.email" />
    <input type="password" [formField]="loginForm.password" />
  `,
})
export class LoginComponent {
  loginModel = signal({
    email: '',
    password: '',
  });

  loginForm = form(this.loginModel);
}

form()関数はモデルのシグナルを受け取り、モデルの形状を反映した特別なオブジェクト構造であるフィールドツリーを作成します。フィールドツリーは、ナビゲート可能(loginForm.emailのようにドット記法で子フィールドにアクセス)であり、呼び出し可能(フィールドを関数として呼び出してその状態にアクセス)でもあります。

[formField]ディレクティブは、各入力要素をフィールドツリー内の対応するフィールドにバインドし、UIとモデル間の自動的な双方向同期を可能にします。

TypeScriptの型を使用する

TypeScriptはオブジェクトリテラルから型を推論しますが、明示的な型を定義することでコードの品質が向上し、より良いIntelliSenseのサポートが提供されます。

interface LoginData {
  email: string;
  password: string;
}

export class LoginComponent {
  loginModel = signal<LoginData>({
    email: '',
    password: '',
  });

  loginForm = form(this.loginModel);
}

明示的な型を使用すると、フィールドツリーは完全な型安全性を提供します。loginForm.emailへのアクセスはFieldTree<string>として型付けされ、存在しないプロパティにアクセスしようとするとコンパイル時エラーが発生します。

// TypeScript knows this is FieldTree<string>
const emailField = loginForm.email;

// TypeScript error: Property 'username' does not exist
const usernameField = loginForm.username;

すべてのフィールドを初期化する

フォームモデルは、フィールドツリーに含めたいすべてのフィールドに初期値を提供する必要があります。

Prefer
// Good: All fields initialized
const userModel = signal({
  name: '',
  email: '',
  age: 0,
});
Avoid
// Avoid: Missing initial value
const userModel = signal({
  name: '',
  email: '',
  // age field is not defined - cannot access userForm.age
});

オプショナルなフィールドについては、明示的に空の値またはnullを設定してください:

interface UserData {
  name: string;
  email: string;
  phoneNumber: string | null;
}

const userModel = signal<UserData>({
  name: '',
  email: '',
  phoneNumber: null,
});

HELPFUL: <input type=text><textarea>のようなネイティブテキストコントロールはnullをサポートしていないため、空の値を表すには''を使用してください。

undefinedに設定されたフィールドは、フィールドツリーから除外されます。{value: undefined}を持つモデルは{}と全く同じように動作し、そのフィールドにアクセスするとFieldTreeではなくundefinedが返されます。

モデルの値を読み取る

フォームの値には、モデルのシグナルから直接アクセスする方法と、個々のフィールドを介してアクセスする方法の2つがあります。それぞれのアプローチは異なる目的を果たします。

モデルから読み取る

フォームの送信時など、完全なフォームデータが必要な場合は、モデルのシグナルにアクセスします:

async onSubmit() {
  const formData = this.loginModel();
  console.log(formData.email, formData.password);

  // Send to server
  await this.authService.login(formData);
}

モデルのシグナルはデータオブジェクト全体を返すため、フォームの完全な状態を扱う操作に最適です。

フィールドの状態から読み取る

フィールドツリー内の各フィールドは関数です。フィールドを呼び出すと、フィールドの値、バリデーションステータス、インタラクションの状態に対するリアクティブなシグナルを含むFieldStateオブジェクトが返されます。

テンプレートやリアクティブな計算で個々のフィールドを扱う場合は、フィールドの状態にアクセスします:

@Component({
  template: `
    <p>Current email: {{ loginForm.email().value() }}</p>
    <p>Password length: {{ passwordLength() }}</p>
  `,
})
export class LoginComponent {
  loginModel = signal({email: '', password: ''});
  loginForm = form(this.loginModel);

  passwordLength = computed(() => {
    return this.loginForm.password().value().length;
  });
}

フィールドの状態は、各フィールドの値に対するリアクティブなシグナルを提供するため、フィールド固有の情報を表示したり、派生状態を作成したりするのに適しています。

TIP: フィールドの状態には、value()以外にも、バリデーションの状態(例: valid、invalid、errors)、インタラクションの追跡(例: touched、dirty)、可視性(例: hidden、disabled)など、さらに多くのシグナルが含まれています。

フォームモデルをプログラム的に更新する

set()でフォームモデルを置き換える

フォームモデルでset()を使用して、値全体を置き換えます:

loadUserData() {
  this.userModel.set({
    name: 'Alice',
    email: 'alice@example.com',
    age: 30,
  });
}

resetForm() {
  this.userModel.set({
    name: '',
    email: '',
    age: 0,
  });
}

このアプローチは、APIからデータを読み込む場合や、フォーム全体をリセットする場合に適しています。

set()またはupdate()で単一のフィールドを直接更新する

個々のフィールドの値にset()を使用して、フィールドの状態を直接更新します:

clearEmail() {
  this.userForm.email().value.set('');
}

incrementAge() {
  this.userForm.age().value.update(currentAge => currentAge + 1);
}

これらは「フィールドレベルの更新」としても知られています。これらは自動的にモデルのシグナルに伝播し、両方を同期させ続けます。

例: APIからデータを読み込む

一般的なパターンは、データを取得してモデルに投入することです:

export class UserProfileComponent {
  userModel = signal({
    name: '',
    email: '',
    bio: '',
  });

  userForm = form(this.userModel);
  private userService = inject(UserService);

  ngOnInit() {
    this.loadUserProfile();
  }

  async loadUserProfile() {
    const userData = await this.userService.getUserProfile();
    this.userModel.set(userData);
  }
}

モデルが変更されるとフォームのフィールドは自動的に更新され、追加のコードなしで取得したデータを表示します。

双方向データバインディング

[formField]ディレクティブは、モデル、フォームの状態、UIの間で自動的な双方向の同期を作成します。

データフローの仕組み

変更は双方向に流れます:

ユーザー入力 → モデル:

  1. ユーザーが入力要素に入力する
  2. [formField]ディレクティブが変更を検知する
  3. フィールドの状態が更新される
  4. モデルのシグナルが更新される

プログラムによる更新 → UI:

  1. コードがset()またはupdate()でモデルを更新する
  2. モデルのシグナルがサブスクライバーに通知する
  3. フィールドの状態が更新される
  4. [formField]ディレクティブが入力要素を更新する

この同期は自動的に行われます。モデルとUIを同期させるために、サブスクリプションやイベントハンドラーを記述する必要はありません。

例: 両方向

@Component({
  template: `
    <input type="text" [formField]="userForm.name" />
    <button (click)="setName('Bob')">Set Name to Bob</button>
    <p>Current name: {{ userModel().name }}</p>
  `,
})
export class UserComponent {
  userModel = signal({name: ''});
  userForm = form(this.userModel);

  setName(name: string) {
    this.userForm.name().value.set(name);
    // Input automatically displays 'Bob'
  }
}

ユーザーが入力フィールドに入力すると、userModel().nameが更新されます。ボタンがクリックされると、入力値は"Bob"に変わります。手動での同期コードは必要ありません。

モデル構造のパターン

フォームモデルは、フラットなオブジェクトにすることも、ネストされたオブジェクトや配列を含めることもできます。選択する構造は、フィールドへのアクセス方法やバリデーションの構成に影響します。

フラットモデルとネストモデル

フラットなフォームモデルは、すべてのフィールドをトップレベルに保持します:

// Flat structure
const userModel = signal({
  name: '',
  email: '',
  street: '',
  city: '',
  state: '',
  zip: '',
});

ネストされたモデルは、関連するフィールドをグループ化します:

// Nested structure
const userModel = signal({
  name: '',
  email: '',
  address: {
    street: '',
    city: '',
    state: '',
    zip: '',
  },
});

次のような場合は、フラットな構造を使用します:

  • フィールドに明確な概念的なグループ分けがない場合
  • フィールドへのアクセスをよりシンプルにしたい場合 (userForm.city vs userForm.address.city)
  • バリデーションルールが複数の潜在的なグループにまたがる場合

次のような場合は、ネストされた構造を使用します:

  • フィールドが明確な概念的なグループ(住所など)を形成する場合
  • グループ化されたデータがAPI構造と一致する場合
  • グループを1つの単位としてバリデーションしたい場合

ネストされたオブジェクトの操作

オブジェクトパスをたどることで、ネストされたフィールドにアクセスできます:

const userModel = signal({
  profile: {
    firstName: '',
    lastName: '',
  },
  settings: {
    theme: 'light',
    notifications: true,
  },
});

const userForm = form(userModel);

// Access nested fields
userForm.profile.firstName; // FieldTree<string>
userForm.settings.theme; // FieldTree<string>

テンプレートでは、トップレベルのフィールドと同じ方法でネストされたフィールドをバインドします:

@Component({
  template: `
    <input [formField]="userForm.profile.firstName" />
    <input [formField]="userForm.profile.lastName" />

    <select [formField]="userForm.settings.theme">
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  `,
})

配列の操作

モデルには、アイテムのコレクションとして配列を含めることができます:

const orderModel = signal({
  customerName: '',
  items: [{product: '', quantity: 0, price: 0}],
});

const orderForm = form(orderModel);

// Access array items by index
orderForm.items[0].product; // FieldTree<string>
orderForm.items[0].quantity; // FieldTree<number>

オブジェクトを含む配列のアイテムは自動的に追跡IDを受け取ります。これにより、配列内でアイテムの位置が変わってもフィールドの状態を維持できます。これにより、配列が並べ替えられた場合でも、バリデーションの状態とユーザーインタラクションが正しく維持されることが保証されます。

次のステップ

このガイドでは、モデルの作成と値の更新について説明しました。関連ガイドでは、シグナルフォームの他の側面について探求します: