詳細ガイド
フォーム

フィールドの状態管理

シグナルフォームのフィールドの状態は、バリデーションの状態(validinvaliderrorsなど)、インタラクションの追跡(toucheddirtyなど)、可用性(disabledhiddenなど)のためのリアクティブなシグナルを提供し、ユーザーのインタラクションに反応できるようにします。

フィールドの状態を理解する

form()関数でフォームを作成すると、フィールドツリーが返されます。これはフォームモデルを反映したオブジェクト構造です。ツリー内の各フィールドには、ドット記法(form.emailなど)でアクセスできます。

フィールドの状態へのアクセス

フィールドツリー内の任意のフィールドを関数として(form.email()のように)呼び出すと、FieldStateオブジェクトが返されます。これには、フィールドのバリデーション、インタラクション、および可用性の状態を追跡するリアクティブなシグナルが含まれています。たとえば、invalid()シグナルは、フィールドにバリデーションエラーがあるかどうかを示します:

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

@Component({
  selector: 'app-registration',
  imports: [FormField],
  template: `
    <input type="email" [formField]="registrationForm.email" />

    @if (registrationForm.email().invalid()) {
      <p class="error">Email has validation errors:</p>
      <ul>
        @for (error of registrationForm.email().errors(); track error) {
          <li>{{ error.message }}</li>
        }
      </ul>
    }
  `,
})
export class Registration {
  registrationModel = signal({
    email: '',
    password: '',
  });

  registrationForm = form(this.registrationModel, (schemaPath) => {
    required(schemaPath.email, {message: 'Email is required'});
    email(schemaPath.email, {message: 'Enter a valid email address'});
  });
}

この例では、テンプレートはregistrationForm.email().invalid()をチェックして、エラーメッセージを表示するかどうかを判断します。

フィールドの状態シグナル

最も一般的に使用されるシグナルはvalue()です。これはフィールドの現在の値へのアクセスを提供するWritableSignalです:

const emailValue = registrationForm.email().value();
console.log(emailValue); // Current email string

value()に加えて、フィールドの状態には、バリデーション、インタラクションの追跡、および可用性の制御のためのシグナルが含まれています:

カテゴリー シグナル 説明
バリデーション valid() フィールドがすべてのバリデーションルールに合格し、保留中のバリデーターがない
invalid() フィールドにバリデーションエラーがある
errors() バリデーションエラーオブジェクトの配列
pending() 非同期バリデーションが進行中
インタラクション touched() ユーザーがフィールドにフォーカスし、フォーカスを外した(インタラクティブな場合)
dirty() ユーザーがフィールドを変更した(インタラクティブな場合)。値が初期状態と一致していても同様
可用性 disabled() フィールドが無効化されており、親フォームの状態に影響しない
hidden() フィールドを非表示にすることを示す。テンプレートでの可視性は@ifで制御される
readonly() フィールドが読み取り専用であり、親フォームの状態に影響しない

これらのシグナルを使用すると、ユーザーの行動に反応するレスポンシブなフォームのユーザー体験を構築できます。以下のセクションでは、各カテゴリーを詳しく説明します。

バリデーション状態

バリデーション状態シグナルは、フィールドが有効かどうか、またどのようなエラーを含んでいるかを示します。

NOTE: このガイドでは、テンプレートやロジックでバリデーション状態を使用すること(フィードバックを表示するためにvalid()invalid()errors()を読み取るなど)に焦点を当てています。バリデーションルールを定義したり、カスタムバリデーターを作成したりする方法については、バリデーションガイドを参照してください。

有効性のチェック

valid()invalid()を使用してバリデーションステータスをチェックします:

@Component({
  template: `
    <input type="email" [formField]="loginForm.email" />

    @if (loginForm.email().invalid()) {
      <p class="error">Email is invalid</p>
    }
    @if (loginForm.email().valid()) {
      <p class="success">Email looks good</p>
    }
  `,
})
export class Login {
  loginModel = signal({email: '', password: ''});
  loginForm = form(this.loginModel);
}
シグナル 次の場合にtrueを返します
valid() フィールドがすべてのバリデーションルールに合格し、保留中のバリデーターがない場合
invalid() フィールドにバリデーションエラーがある場合

コードで有効性をチェックする際、「エラーがある」と「バリデーションが保留中」を区別したい場合は、!valid()の代わりにinvalid()を使用してください。これは、非同期バリデーションが保留中の場合、valid()invalid()の両方が同時にfalseになる可能性があるためです。バリデーションが完了していないためフィールドはまだ有効ではなく、またエラーがまだ見つかっていないため無効でもありません。

バリデーションエラーの読み取り

errors()でバリデーションエラーの配列にアクセスします。各エラーオブジェクトには以下が含まれます:

プロパティ 説明
kind 失敗したバリデーションルール("required"や"email"など)
message オプションの人間が読める形式のエラーメッセージ
fieldTree エラーが発生したFieldTreeへの参照

NOTE: messageプロパティはオプションです。バリデーターはカスタムエラーメッセージを提供できますが、指定されていない場合は、エラーのkind値を独自メッセージにマッピングする必要があるかもしれません。

以下は、テンプレートでエラーを表示する方法の例です:

@Component({
  template: `
    <input type="email" [formField]="loginForm.email" />

    @if (loginForm.email().errors().length > 0) {
      <div class="errors">
        @for (error of loginForm.email().errors(); track error) {
          <p>{{ error.message }}</p>
        }
      </div>
    }
  `
})

このアプローチでは、フィールドのすべてのエラーをループ処理し、各エラーメッセージをユーザーに表示します。

保留中のバリデーション

pending()シグナルは、非同期バリデーションが進行中であることを示します:

@Component({
  template: `
    <input type="email" [formField]="signupForm.email" />

    @if (signupForm.email().pending()) {
      <p>Checking if email is available...</p>
    }

    @if (signupForm.email().invalid() && !signupForm.email().pending()) {
      <p>Email is already taken</p>
    }
  `
})

このシグナルにより、非同期バリデーションの実行中にローディング状態を表示できます。

インタラクション状態

インタラクション状態は、ユーザーがフィールドを操作したかどうかを追跡し、「ユーザーがフィールドに触れた後にのみエラーを表示する」といったパターンを可能にします。

Touched状態

touched()シグナルは、ユーザーがフィールドにフォーカスし、その後ブラーしたかどうかを追跡します。ユーザー操作によって(プログラム的にではなく)フィールドにフォーカスし、その後ブラーするとtrueになります。非表示、無効、読み取り専用のフィールドは非インタラクティブであり、ユーザー操作によってtouchedになることはありません。

Dirty状態

フォームでは、データが実際に変更されたかどうかを検出する必要があることがよくあります。たとえば、未保存の変更についてユーザーに警告したり、必要な場合にのみ保存ボタンを有効にしたりするためです。dirty()シグナルは、ユーザーがフィールドを変更したかどうかを追跡します。

dirty()シグナルは、ユーザーがインタラクティブなフィールドの値を変更するとtrueになり、値を元の値に戻してもtrueのままです:

@Component({
  template: `
    <form novalidate>
      <input [formField]="profileForm.name" />
      <input [formField]="profileForm.bio" />

      @if (profileForm().dirty()) {
        <p class="warning">You have unsaved changes</p>
      }
    </form>
  `,
})
export class Profile {
  profileModel = signal({name: 'Alice', bio: 'Developer'});
  profileForm = form(this.profileModel);
}

「未保存の変更」の警告や、データが変更された場合にのみ保存ボタンを有効にするには、dirty()を使用します。

Touchedとdirtyの比較

これらのシグナルは、異なるユーザーインタラクションを追跡します:

シグナル trueになる条件
touched() ユーザーがインタラクティブなフィールドにフォーカスしてブラーしたとき(何も変更しなくても)
dirty() ユーザーがインタラクティブなフィールドを変更したとき(一度もブラーしなくても、また現在の値が初期値と一致していても)

フィールドは、さまざまな組み合わせの状態になり得ます:

状態 シナリオ
Touchedだがdirtyではない ユーザーがフィールドにフォーカスしてブラーしたが、変更はしなかった
Touchedかつdirty ユーザーがフィールドにフォーカスし、値を変更してブラーした

NOTE: 非表示、無効、読み取り専用のフィールドは非インタラクティブです。これらはユーザー操作によってtouchedやdirtyになることはありません。

可用性状態

可用性状態シグナルは、フィールドがインタラクティブか、編集可能か、表示されるかを制御します。無効、非表示、読み取り専用のフィールドは非インタラクティブです。これらは、親フォームが有効か、touchedか、dirtyかには影響しません。

無効なフィールド

disabled()シグナルは、フィールドがユーザー入力を受け入れるかどうかを示します。無効なフィールドはUIに表示されますが、ユーザーはそれらを操作できません。

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

@Component({
  selector: 'app-order',
  imports: [FormField],
  template: `
    <!-- TIP: `[formField]`ディレクティブは、フィールドの`disabled()`状態に基づいて`disabled`属性を自動的にバインドするため、手動で`[disabled]="field().disabled()"`を追加する必要はありません -->
    <input [formField]="orderForm.couponCode" />

    @if (orderForm.couponCode().disabled()) {
      <p class="info">クーポンコードは50ドルを超える注文でのみ利用可能です</p>
    }
  `
})
export class Order {
  orderModel = signal({
    total: 25,
    couponCode: ''
  })

  orderForm = form(this.orderModel, schemaPath => {
    disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50)
  })
}

この例では、valueOf(schemaPath.total)を使用してtotalフィールドの値をチェックし、couponCodeを無効にするべきかどうかを判断します。

NOTE: スキーマのコールバックパラメータ(この例ではschemaPath)は、フォーム内のすべてのフィールドへのパスを提供するSchemaPathTreeオブジェクトです。このパラメータには好きな名前を付けることができます。

disabled()hidden()readonly()のようなルールを定義する場合、ロジックコールバックは通常、分割代入される(({valueOf})など)FieldContextオブジェクトを受け取ります。バリデーションルールで一般的に使用される2つのメソッドは次のとおりです:

  • valueOf(schemaPath.otherField) - フォーム内の別のフィールドの値を読み取ります
  • value() - ルールが適用されるフィールドの値を含むシグナル

無効なフィールドは、親フォームのバリデーション状態には影響しません。無効なフィールドが不正な値であっても、親フォームは有効になり得ます。disabled()状態はインタラクティブ性とバリデーションに影響しますが、フィールドの値は変更しません。

非表示のフィールド

hidden()シグナルは、フィールドが条件付きで非表示になるかどうかを示します。条件に基づいてフィールドを表示または非表示にするには、@ifと共にhidden()を使用します:

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

@Component({
  selector: 'app-profile',
  imports: [FormField],
  template: `
    <label>
      <input type="checkbox" [formField]="profileForm.isPublic" />
      プロフィールを公開する
    </label>

    @if (!profileForm.publicUrl().hidden()) {
      <label>
        公開URL
        <input [formField]="profileForm.publicUrl" />
      </label>
    }
  `,
})
export class Profile {
  profileModel = signal({
    isPublic: false,
    publicUrl: '',
  });

  profileForm = form(this.profileModel, (schemaPath) => {
    hidden(schemaPath.publicUrl, ({valueOf}) => !valueOf(schemaPath.isPublic));
  });
}

非表示のフィールドはバリデーションに参加しません。必須フィールドが非表示の場合でも、フォームの送信は妨げられません。hidden()状態は可用性とバリデーションに影響しますが、フィールドの値は変更しません。

読み取り専用フィールド

readonly()シグナルは、フィールドが読み取り専用かどうかを示します。読み取り専用フィールドは値を表示しますが、ユーザーは編集できません:

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

@Component({
  selector: 'app-account',
  imports: [FormField],
  template: `
    <label>
      ユーザー名(変更不可)
      <input [formField]="accountForm.username" />
    </label>

    <label>
      メールアドレス
      <input [formField]="accountForm.email" />
    </label>
  `,
})
export class Account {
  accountModel = signal({
    username: 'johndoe',
    email: 'john@example.com',
  });

  accountForm = form(this.accountModel, (schemaPath) => {
    readonly(schemaPath.username);
  });
}

NOTE: [formField]ディレクティブは、フィールドのreadonly()状態に基づいてreadonly属性を自動的にバインドするため、手動で[readonly]="field().readonly()"を追加する必要はありません。

無効フィールドや非表示フィールドと同様に、読み取り専用フィールドは非インタラクティブであり、親フォームの状態に影響を与えません。readonly()状態は編集可能性とバリデーションに影響しますが、フィールドの値は変更しません。

それぞれをいつ使用するか

状態 使用する状況 ユーザーに表示されるか ユーザーが操作できるか バリデーションに寄与するか
disabled() フィールドが一時的に利用できない場合(他のフィールド値に基づくなど) はい いいえ いいえ
hidden() 現在のコンテキストでフィールドが関連しない場合 いいえ(@ifを使用) いいえ いいえ
readonly() 値は表示されるべきだが、編集はできない場合 はい いいえ いいえ

フォームレベルの状態

ルートフォームもフィールドツリー内のフィールドです。それを関数として呼び出すと、すべての子フィールドの状態を集約したFieldStateオブジェクトも返されます。

フォームの状態へのアクセス

@Component({
  template: `
    <form novalidate>
      <input [formField]="loginForm.email" />
      <input [formField]="loginForm.password" />

      <button [disabled]="!loginForm().valid()">Sign In</button>
    </form>
  `,
})
export class Login {
  loginModel = signal({email: '', password: ''});
  loginForm = form(this.loginModel);
}

この例では、すべての子フィールドが有効な場合にのみフォームが有効になります。これにより、フォーム全体の有効性に基づいて送信ボタンを有効化/無効化できます。

フォームレベルのシグナル

ルートフォームはフィールドであるため、同じシグナル(valid()invalid()touched()dirty()など)を持ちます。

シグナル フォームレベルの動作
valid() すべてのインタラクティブなフィールドが有効で、保留中のバリデーターがない
invalid() 少なくとも1つのインタラクティブなフィールドにバリデーションエラーがある
pending() 少なくとも1つのインタラクティブなフィールドに保留中の非同期バリデーションがある
touched() ユーザーが少なくとも1つのインタラクティブなフィールドに触れた
dirty() ユーザーが少なくとも1つのインタラクティブなフィールドを変更した

フォームレベルとフィールドレベルの使い分け

フォームレベルの状態は次の場合に使用します:

  • 送信ボタンの有効/無効状態
  • 「保存」ボタンの状態
  • フォーム全体の有効性チェック
  • 未保存の変更に関する警告

フィールドレベルの状態は次の場合に使用します:

  • 個々のフィールドのエラーメッセージ
  • フィールド固有のスタイリング
  • フィールドごとのバリデーションフィードバック
  • 条件付きのフィールドの可用性

状態の伝播

フィールドの状態は、子フィールドから親フィールドグループを通じてルートフォームまで伝播します。

子の状態が親フォームに与える影響

子フィールドが無効になると、その親フィールドグループも無効になり、ルートフォームも同様に無効になります。子がtouchedまたはdirtyになると、親フィールドグループとルートフォームはその変更を反映します。この集約により、フィールドやフォーム全体など、あらゆるレベルで有効性をチェックできます。

const userModel = signal({
  profile: {
    firstName: '',
    lastName: '',
  },
  address: {
    street: '',
    city: '',
  },
});

const userForm = form(userModel);

// firstNameが無効な場合、profileも無効になります
userForm.profile.firstName().invalid() === true;
// → userForm.profile().invalid() === true
// → userForm().invalid() === true

非表示、無効、読み取り専用のフィールド

非表示、無効、読み取り専用のフィールドは非インタラクティブであり、親フォームの状態に影響を与えません:

const orderModel = signal({
  customerName: '',
  requiresShipping: false,
  shippingAddress: '',
});

const orderForm = form(orderModel, (schemaPath) => {
  hidden(schemaPath.shippingAddress, ({valueOf}) => !valueOf(schemaPath.requiresShipping));
});

この例では、shippingAddressが非表示の場合、フォームの有効性には影響しません。その結果、shippingAddressが空で必須であっても、フォームは有効になり得ます。

この動作により、非表示、無効、または読み取り専用のフィールドがフォームの送信をブロックしたり、有効性、touched、dirtyの状態に影響を与えたりするのを防ぎます。

テンプレートでの状態の使用

フィールド状態シグナルはAngularテンプレートとシームレスに統合され、手動のイベントハンドリングなしでリアクティブなフォームのユーザー体験を可能にします。

条件付きのエラー表示

ユーザーがフィールドを操作した後にのみエラーを表示します:

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

@Component({
  selector: 'app-signup',
  imports: [FormField],
  template: `
    <label>
      Email
      <input type="email" [formField]="signupForm.email" />
    </label>

    @if (signupForm.email().touched() && signupForm.email().invalid()) {
      <p class="error">{{ signupForm.email().errors()[0].message }}</p>
    }
  `,
})
export class Signup {
  signupModel = signal({email: '', password: ''});

  signupForm = form(this.signupModel, (schemaPath) => {
    email(schemaPath.email);
  });
}

このパターンは、ユーザーがフィールドを操作する機会を得る前にエラーが表示されるのを防ぎます。エラーは、ユーザーがフィールドにフォーカスしてから離れた後にのみ表示されます。

条件付きのフィールドの可用性

hidden()シグナルを@ifとともに使用して、条件付きでフィールドを表示または非表示にします:

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

@Component({
  selector: 'app-order',
  imports: [FormField],
  template: `
    <label>
      <input type="checkbox" [formField]="orderForm.requiresShipping" />
      Requires shipping
    </label>

    @if (!orderForm.shippingAddress().hidden()) {
      <label>
        Shipping Address
        <input [formField]="orderForm.shippingAddress" />
      </label>
    }
  `,
})
export class Order {
  orderModel = signal({
    requiresShipping: false,
    shippingAddress: '',
  });

  orderForm = form(this.orderModel, (schemaPath) => {
    hidden(schemaPath.shippingAddress, ({valueOf}) => !valueOf(schemaPath.requiresShipping));
  });
}

非表示のフィールドはバリデーションに参加しないため、たとえ非表示のフィールドがそうでなければ無効であってもフォームを送信できます。

Tracking values for array fields

In signal forms, a @for block over a set of fields should be tracked by field identity.

@Component({
  imports: [FormField],
  template: `
    @for (field of form.emails; track field) {
      <input [formField]="field" />
    }
  `,
})
export class App {
  formModel = signal({emails: ['john.doe@mail.com', 'max.musterman@mail.com']});
  form = form(this.formModel);
}

The forms system is already tracking the model values within the array and maintaining a stable identity of the fields it creates automatically.

When an item changes, it may represent a new logical entity even if some of its properties look the same. Tracking by identity ensures the framework treats it as a distinct item rather than reusing existing UI elements. This prevents stateful elements, like form inputs, from being incorrectly shared and keeps bindings aligned with the correct part of the model.

コンポーネントロジックでのフィールド状態の使用

フィールド状態のシグナルは、Angularのリアクティブプリミティブであるcomputed()effect()と連携して、高度なフォームロジックを実現します。

送信前のバリデーションチェック

コンポーネントメソッドでフォームの有効性をチェックします:

export class Registration {
  registrationModel = signal({
    username: '',
    email: '',
    password: '',
  });

  registrationForm = form(this.registrationModel);

  async onSubmit() {
    // Wait for any pending async validation
    if (this.registrationForm().pending()) {
      console.log('Waiting for validation...');
      return;
    }

    // Guard against invalid submissions
    if (this.registrationForm().invalid()) {
      console.error('Form is invalid');
      return;
    }

    const data = this.registrationModel();
    await this.api.register(data);
  }
}

これにより、有効で完全にバリデーションされたデータのみがAPIに到達することが保証されます。

computedによる派生状態

フィールド状態に基づいてcomputedシグナルを作成すると、基礎となるフィールドの状態が変化したときに自動的に更新されます:

export class Password {
  passwordModel = signal({password: '', confirmPassword: ''});
  passwordForm = form(this.passwordModel);

  // Compute password strength indicator
  passwordStrength = computed(() => {
    const password = this.passwordForm.password().value();
    if (password.length < 8) return 'weak';
    if (password.length < 12) return 'medium';
    return 'strong';
  });

  // Check if all required fields are filled
  allFieldsFilled = computed(() => {
    return (
      this.passwordForm.password().value().length > 0 &&
      this.passwordForm.confirmPassword().value().length > 0
    );
  });
}

プログラムによる状態変更

フィールドの状態は通常、ユーザーインタラクション(タイピング、フォーカス、ブラー)によって更新されますが、プログラムで制御する必要がある場合もあります。一般的なシナリオには、フォームの送信やフォームのリセットが含まれます。

フォームの送信

シグナルフォームprovides a FormRoot directive that simplifies form submission. It automatically prevents the default browser form submission behavior and sets the novalidate attribute on the <form> element.

@Component({
  imports: [FormRoot, FormField],
  template: `
    <form [formRoot]="registrationForm">
      <input [formField]="registrationForm.username" />
      <input type="email" [formField]="registrationForm.email" />
      <input type="password" [formField]="registrationForm.password" />

      <button type="submit">Register</button>
    </form>
  `,
})
export class Registration {
  registrationModel = signal({username: '', email: '', password: ''});

  registrationForm = form(
    this.registrationModel,
    (schemaPath) => {
      required(schemaPath.username);
      email(schemaPath.email);
      required(schemaPath.password);
    },
    {
      submission: {
        action: async () => this.submitToServer(),
      },
    },
  );

  private submitToServer() {
    // Send data to server
  }
}

When you use FormRoot, submitting the form automatically calls the submit() function, which marks all fields as touched (revealing validation errors) and executes your action callback if the form is valid.

You can also submit a form manually, without using the directive, by calling submit(this.registrationForm). When explicitly calling the submit function like this, you can pass a FormSubmitOptions to override the default submission logic for the form: submit(this.registrationForm, {action: () => /* ... */ }).

送信後のフォームのリセット

After successfully submitting a form, you may want to return it to its initial state - clearing both user interaction history and field values. The reset() method clears the touched and dirty flags. You can also pass an optional value to reset() to update the model data:

export class Contact {
  private readonly INITIAL_MODEL = {name: '', email: '', message: ''};
  contactModel = signal({...this.INITIAL_MODEL});
  contactForm = form(this.contactModel, {
    submission: {
      action: async (f) => {
        await this.api.sendMessage(this.contactModel());
        // Clear interaction state (touched, dirty) and reset to initial values
        f().reset({...this.INITIAL_MODEL});
      },
    },
  });
}

This ensures the form is ready for new input without showing stale error messages or dirty state indicators.

バリデーション状態に基づいたスタイリング

バリデーション状態に基づいてCSSクラスをバインドすることで、フォームにカスタムスタイルを適用できます:

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

@Component({
  template: `
    <input
      type="email"
      [formField]="form.email"
      [class.is-invalid]="form.email().touched() && form.email().invalid()"
      [class.is-valid]="form.email().touched() && form.email().valid()"
    />
  `,
  styles: `
    input.is-invalid {
      border: 2px solid red;
      background-color: white;
    }

    input.is-valid {
      border: 2px solid green;
    }
  `,
})
export class StyleExample {
  model = signal({email: ''});

  form = form(this.model, (schemaPath) => {
    email(schemaPath.email);
  });
}

touched()とバリデーション状態の両方をチェックすることで、ユーザーがフィールドを操作した後にのみスタイルが表示されるようになります。

次のステップ

このガイドでは、バリデーションと可用性ステータスの処理、インタラクションの追跡、フィールド状態の伝播について説明しました。関連ガイドでは、シグナルフォームの他の側面について探求します: