NOTE: このガイドは、Signal Formsの基本に精通していることを前提としています。
ブラウザの組み込みフォームコントロール(input、select、textareaなど)は一般的なケースを扱いますが、アプリケーションではしばしば特殊な入力が必要になります。カレンダーUIを持つ日付ピッカー、書式設定ツールバーを持つリッチテキストエディタ、オートコンプリート機能を持つタグセレクターなどは、すべてカスタム実装が必要です。
シグナルフォームは、特定のインターフェースを実装するあらゆるコンポーネントと連携して動作します。コントロールインターフェースは、コンポーネントがフォームシステムと通信するためのプロパティとシグナルを定義します。コンポーネントがこれらのインターフェースのいずれかを実装すると、[field]ディレクティブが自動的にコントロールをフォームの状態、バリデーション、データバインディングに接続します。
基本的なカスタムコントロールの作成
最小限の実装から始めて、必要に応じて機能を追加していきましょう。
最小限の入力コントロール
基本的なカスタム入力は、FormValueControlインターフェースを実装し、必須のvalueモデルシグナルを定義するだけで済みます。
import { Component, model } from '@angular/core';import { FormValueControl } from '@angular/forms/signals';@Component({ selector: 'app-basic-input', template: ` <div class="basic-input"> <input type="text" [value]="value()" (input)="value.set(($event.target as HTMLInputElement).value)" placeholder="Enter text..." /> </div> `,})export class BasicInput implements FormValueControl<string> { /** The current input value */ value = model('');}
最小限のチェックボックスコントロール
チェックボックス形式のコントロールには、次の2つが必要です:
Fieldディレクティブがフォームコントロールとして認識できるように、FormCheckboxControlインターフェースを実装するcheckedモデルシグナルを提供する
import { Component, model, ChangeDetectionStrategy } from '@angular/core';import { FormCheckboxControl } from '@angular/forms/signals';@Component({ selector: 'app-basic-toggle', template: ` <button type="button" [class.active]="checked()" (click)="toggle()" > <span class="toggle-slider"></span> </button> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class BasicToggle implements FormCheckboxControl { /** Whether the toggle is checked */ checked = model<boolean>(false); toggle() { this.checked.update(val => !val); }}
カスタムコントロールの使用
コントロールを作成したら、Fieldディレクティブを追加することで、組み込みの入力を使用する場所ならどこでも使用できます:
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';import { form, Field, required } from '@angular/forms/signals';import { BasicInput } from './basic-input';import { BasicToggle } from './basic-toggle';@Component({ imports: [Field, BasicInput, BasicToggle], template: ` <form> <label> Email <app-basic-input [field]="registrationForm.email" /> </label> <label> Accept terms <app-basic-toggle [field]="registrationForm.acceptTerms" /> </label> <button type="submit" [disabled]="registrationForm().invalid()" > Register </button> </form> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class Registration { registrationModel = signal({ email: '', acceptTerms: false }); registrationForm = form(this.registrationModel, (schemaPath) => { required(schemaPath.email, { message: 'Email is required' }); required(schemaPath.acceptTerms, { message: 'You must accept the terms' }); });}
NOTE: スキーマのコールバックパラメータ(この例ではschemaPath)は、フォーム内のすべてのフィールドへのパスを提供するSchemaPathTreeオブジェクトです。このパラメータには好きな名前を付けることができます。
[field]ディレクティブは、カスタムコントロールと組み込みの入力で同じように動作します。シグナルフォームはそれらを同じように扱います - バリデーションの実行、状態の更新、データバインディングが自動的に機能します。
コントロールインターフェースの理解
カスタムコントロールの動作を確認したところで、それらがシグナルフォームとどのように統合されるかを見ていきましょう。
コントロールインターフェース
作成したBasicInputとBasicToggleコンポーネントは、シグナルフォームにそれらとの対話方法を伝える特定のコントロールインターフェースを実装しています。
FormValueControl
FormValueControlは、テキスト入力、数値入力、日付ピッカー、セレクトドロップダウンなど、単一の値を編集するほとんどの入力タイプのためのインターフェースです。コンポーネントがこのインターフェースを実装する場合:
- 必須プロパティ: コンポーネントは
valueモデルシグナルを提供する必要があります - Fieldディレクティブの役割: フォームフィールドの値をコントロールの
valueシグナルにバインドします
IMPORTANT: FormValueControlを実装するコントロールはcheckedプロパティを持ってはいけません
FormCheckboxControl
FormCheckboxControlは、トグル、スイッチなど、ブール値のオン/オフ状態を表すチェックボックスのようなコントロールのためのインターフェースです。コンポーネントがこのインターフェースを実装する場合:
- 必須プロパティ: コンポーネントは
checkedモデルシグナルを提供する必要があります - Fieldディレクティブの役割: フォームフィールドの値をコントロールの
checkedシグナルにバインドします
IMPORTANT: FormCheckboxControlを実装するコントロールはvalueプロパティを持ってはいけません
オプションの状態プロパティ
FormValueControlとFormCheckboxControlはどちらもFormUiControlを拡張します。これはフォームの状態と統合するためのオプションのプロパティを提供するベースインターフェースです。
すべてのプロパティはオプションです。コントロールが必要とするものだけを実装してください。
インタラクションの状態
ユーザーがコントロールを操作したときを追跡します:
| プロパティ | 目的 |
|---|---|
touched |
ユーザーがフィールドを操作したかどうか |
dirty |
値が初期状態と異なるかどうか |
バリデーションの状態
ユーザーにバリデーションのフィードバックを表示します:
| プロパティ | 目的 |
|---|---|
errors |
現在のバリデーションエラーの配列 |
valid |
フィールドが有効かどうか |
invalid |
フィールドにバリデーションエラーがあるかどうか |
pending |
非同期バリデーションが進行中かどうか |
可用性の状態
ユーザーがフィールドを操作できるかどうかを制御します:
| プロパティ | 目的 |
|---|---|
disabled |
フィールドが無効かどうか |
disabledReasons |
フィールドが無効になっている理由 |
readonly |
フィールドが読み取り専用(表示されるが編集不可)かどうか |
hidden |
フィールドがビューから隠されているかどうか |
NOTE: disabledReasonsはDisabledReasonオブジェクトの配列です。各オブジェクトはfieldプロパティ(フィールドツリーへの参照)とオプションのmessageプロパティを持ちます。メッセージにはreason.messageを介してアクセスします。
バリデーション制約
フォームからバリデーション制約の値を受け取ります:
| プロパティ | 目的 |
|---|---|
required |
フィールドが必須かどうか |
min |
最小数値(制約がない場合はundefined) |
max |
最大数値(制約がない場合はundefined) |
minLength |
最小の文字列長(制約がない場合はundefined) |
maxLength |
最大の文字列長(制約がない場合はundefined) |
pattern |
一致させる正規表現パターンの配列 |
フィールドのメタデータ
| プロパティ | 目的 |
|---|---|
name |
フィールドのname属性(フォームやアプリケーション全体で一意) |
以下の「状態シグナルの追加」セクションでは、これらのプロパティをコントロールに実装する方法を示します。
Fieldディレクティブの仕組み
[field]ディレクティブは、コントロールがどのインターフェースを実装しているかを検出し、適切なシグナルを自動的にバインドします:
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';import { form, Field, required } from '@angular/forms/signals';import { CustomInput } from './custom-input';import { CustomToggle } from './custom-toggle';@Component({ selector: 'app-my-form', imports: [Field, CustomInput, CustomToggle], template: ` <form> <app-custom-input [field]="userForm.username" /> <app-custom-toggle [field]="userForm.subscribe" /> </form> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class MyForm { formModel = signal({ username: '', subscribe: false }); userForm = form(this.formModel, (schemaPath) => { required(schemaPath.username, { message: 'Username is required' }); });}
TIP: フォームモデルの作成と管理に関する完全な情報については、フォームモデルガイドを参照してください。
[field]="userForm.username"をバインドすると、Fieldディレクティブは次のようになります:
- コントロールが
FormValueControlを実装していることを検出します - 内部で
userForm.username().value()にアクセスし、それをコントロールのvalueモデルシグナルにバインドします - フォームの状態シグナル(
disabled()、errors()など)をコントロールのオプションの入力シグナルにバインドします - 更新はシグナルのリアクティビティを通じて自動的に行われます
状態シグナルの追加
上記の最小限のコントロールは機能しますが、フォームの状態には応答しません。オプションの入力シグナルを追加して、コントロールが無効状態に反応したり、バリデーションエラーを表示したり、ユーザーインタラクションを追跡したりできるようにできます。
以下は、一般的な状態プロパティを実装する包括的な例です:
import { Component, model, input, ChangeDetectionStrategy } from '@angular/core';import { FormValueControl } from '@angular/forms/signals';import type { ValidationError, DisabledReason } from '@angular/forms/signals';@Component({ selector: 'app-stateful-input', template: ` @if (!hidden()) { <div class="input-container"> <input type="text" [value]="value()" (input)="value.set(($event.target as HTMLInputElement).value)" [disabled]="disabled()" [readonly]="readonly()" [class.invalid]="invalid()" [attr.aria-invalid]="invalid()" (blur)="touched.set(true)" /> @if (invalid()) { <div class="error-messages" role="alert"> @for (error of errors(); track error) { <span class="error">{{ error.message }}</span> } </div> } @if (disabled() && disabledReasons().length > 0) { <div class="disabled-reasons"> @for (reason of disabledReasons(); track reason) { <span>{{ reason.message }}</span> } </div> } </div> } `, changeDetection: ChangeDetectionStrategy.OnPush,})export class StatefulInput implements FormValueControl<string> { // Required value = model<string>(''); // Writable interaction state - control updates these touched = model<boolean>(false); // Read-only state - form system manages these disabled = input<boolean>(false); disabledReasons = input<readonly DisabledReason[]>([]); readonly = input<boolean>(false); hidden = input<boolean>(false); invalid = input<boolean>(false); errors = input<readonly ValidationError.WithField[]>([]);}
その結果、バリデーションと状態管理を備えたコントロールを使用できます:
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';import { form, Field, required, email } from '@angular/forms/signals';import { StatefulInput } from './stateful-input';@Component({ imports: [Field, StatefulInput], template: ` <form> <label> Email <app-stateful-input [field]="loginForm.email" /> </label> </form> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class Login { loginModel = signal({ email: '' }); loginForm = form(this.loginModel, (schemaPath) => { required(schemaPath.email, { message: 'Email is required' }); email(schemaPath.email, { message: 'Enter a valid email address' }); });}
ユーザーが無効なメールアドレスを入力すると、Fieldディレクティブが自動的にinvalid()とerrors()を更新します。あなたのコントロールは、そのバリデーションフィードバックを表示できます。
状態プロパティのシグナルタイプ
ほとんどの状態プロパティはinput()(フォームからの読み取り専用)を使用します。コントロールがユーザーインタラクションに応じて更新する場合は、touchedにmodel()を使用します。touchedプロパティは、ニーズに応じてmodel()、input()、またはOutputRefを一意にサポートします。
値の変換
コントロールは、フォームモデルに格納されている値とは異なる形式で値を表示することがあります。例えば、日付ピッカーは「2024-01-15」と格納しながら「January 15, 2024」と表示したり、通貨入力は1234.56と格納しながら「$1,234.56」と表示したりします。
@angular/coreのcomputed()シグナルを使用してモデルの値を表示用に変換し、入力イベントを処理してユーザー入力を格納形式にパースして戻します:
import { Component, model, computed, ChangeDetectionStrategy } from '@angular/core';import { FormValueControl } from '@angular/forms/signals';@Component({ selector: 'app-currency-input', template: ` <input type="text" [value]="displayValue()" (input)="handleInput(($event.target as HTMLInputElement).value)" /> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class CurrencyInput implements FormValueControl<number> { value = model<number>(0); // 数値 (1234.56) を格納します displayValue = computed(() => { return this.value().toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); // 「1,234.56」と表示します }); handleInput(input: string) { const num = parseFloat(input.replace(/[^0-9.]/g, '')); if (!isNaN(num)) this.value.set(num); }}
バリデーションの統合
コントロールはバリデーションの状態を表示しますが、バリデーションは実行しません。バリデーションはフォームスキーマで行われます。コントロールはFieldディレクティブからinvalid()とerrors()シグナルを受け取り、それらを表示します(上記のStatefulInputの例で示されているように)。
Fieldディレクティブは、required、min、max、minLength、maxLength、patternのようなバリデーション制約の値も渡します。コントロールはこれらを使用してUIを強化できます:
export class NumberInput implements FormValueControl<number> { value = model<number>(0); // スキーマのバリデーションルールからの制約値 required = input<boolean>(false); min = input<number | undefined>(undefined); max = input<number | undefined>(undefined);}
スキーマにmin()とmax()のバリデーションルールを追加すると、Fieldディレクティブはこれらの値をコントロールに渡します。これらを使用して、HTML5属性を適用したり、テンプレートに制約のヒントを表示したりします。
IMPORTANT: コントロールにバリデーションロジックを実装しないでください。バリデーションルールはフォームスキーマで定義し、コントロールにはその結果を表示させるようにしてください:
// 悪い例:コントロール内でのバリデーションexport class BadControl implements FormValueControl<string> { value = model<string>('') isValid() { return this.value().length >= 8 } // これは行わないでください!}// 良い例:スキーマでバリデーションし、コントロールは結果を表示accountForm = form(this.accountModel, schemaPath => { minLength(schemaPath.password, 8, { message: 'Password must be at least 8 characters' })})
次のステップ
このガイドでは、シグナルフォームと連携するカスタムコントロールの構築について説明しました。関連ガイドでは、シグナルフォームの他の側面について探求します:
- フォームモデルガイド - フォームモデルの作成と更新