フォームには、ユーザーが送信前に正しく完全なデータを提供することを保証するためにバリデーションが必要です。バリデーションがない場合、サーバー側でデータ品質の問題を処理し、不明瞭なエラーメッセージでユーザー体験を低下させ、すべての制約を手動でチェックする必要があるでしょう。
シグナルフォームは、スキーマベースのバリデーションアプローチを提供します。バリデーションルールはスキーマ関数を使用してフィールドにバインドされ、値が変更されると自動的に実行され、フィールド状態シグナルを通じてエラーを公開します。これにより、ユーザーがフォームを操作するにつれて更新されるリアクティブなバリデーションが可能になります。
バリデーションの基本
シグナルフォームにおけるバリデーションは、form()の第二引数として渡されるスキーマ関数を通じて定義されます。
スキーマ関数
スキーマ関数は、バリデーションルールを定義するためのSchemaPathTreeオブジェクトを受け取ります:
app.ts
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectionStrategy, Component, signal} from '@angular/core';
import {email, form, FormField, required, submit} from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [FormField],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
loginModel = signal<LoginData>({
email: '',
password: '',
});
loginForm = form(this.loginModel, (schemaPath) => {
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Enter a valid email address'});
required(schemaPath.password, {message: 'Password is required'});
});
onSubmit(event: Event) {
event.preventDefault();
submit(this.loginForm, {
action: async () => {
const credentials = this.loginModel();
// In a real app, this would be async:
// await this.authService.login(credentials);
console.log('Logging in with:', credentials);
},
});
}
}
スキーマ関数はフォームの初期化中に一度だけ実行されます。バリデーションルールはスキーマパスパラメータ(schemaPath.emailやschemaPath.passwordなど)を使用してフィールドにバインドされ、フィールドの値が変更されるたびにバリデーションが自動的に実行されます。
NOTE: スキーマのコールバックパラメータ(この例ではschemaPath)は、フォーム内のすべてのフィールドへのパスを提供するSchemaPathTreeオブジェクトです。このパラメータには好きな名前を付けることができます。
バリデーションの仕組み
シグナルフォームのバリデーションは、次のパターンに従います:
- スキーマでバリデーションルールを定義 - スキーマ関数内でバリデーションルールをフィールドにバインドします
- 自動実行 - フィールドの値が変更されるとバリデーションルールが実行されます
- エラーの伝播 - バリデーションエラーはフィールドの状態シグナルを通じて公開されます
- リアクティブな更新 - バリデーションの状態が変化するとUIが自動的に更新されます
インタラクティブなフィールドでは、値が変更されるたびにバリデーションが実行されます。非表示および無効化されたフィールドではバリデーションは実行されません - それらのバリデーションルールは、フィールドが再びインタラクティブになるまでスキップされます。
バリデーションのタイミング
バリデーションルールは次の順序で実行されます:
- 同期バリデーション - 値が変更されると、すべての同期バリデーションルールが実行されます
- 非同期バリデーション - 非同期バリデーションルールは、すべての同期バリデーションルールが成功した後にのみ実行されます
- フィールドの状態更新 -
valid()、invalid()、errors()、pending()シグナルが更新されます
同期バリデーションルール(required()やemail()など)は即座に完了します。非同期バリデーションルール(validateHttp()など)は時間がかかる場合があり、実行中はpending()シグナルをtrueに設定します。
すべてのバリデーションルールは変更のたびに実行されます - バリデーションは最初のエラーで中断されません。フィールドにrequired()とemail()の両方のバリデーションルールがある場合、両方が実行され、両方が同時にエラーを生成する可能性があります。
組み込みのバリデーションルール
シグナルフォームは、一般的なバリデーションシナリオのためのバリデーションルールを提供します。すべての組み込みバリデーションルールは、カスタムエラーメッセージと条件付きロジックのためのオプションオブジェクトを受け入れます。
required()
required()バリデーションルールは、フィールドに値があることを保証します:
import {Component, signal} from '@angular/core';
import {form, FormField, required} from '@angular/forms/signals';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `
<form novalidate>
<label>
Username
<input [formField]="registrationForm.username" />
</label>
<label>
Email
<input type="email" [formField]="registrationForm.email" />
</label>
<button type="submit">Register</button>
</form>
`,
})
export class RegistrationComponent {
registrationModel = signal({
username: '',
email: '',
});
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.username, {message: 'Username is required'});
required(schemaPath.email, {message: 'Email is required'});
});
}
フィールドは次の場合に「空」と見なされます:
| 条件 | 例 |
|---|---|
値がnullである |
null, |
| 値が空文字列である | '' |
条件付きの要件には、whenオプションを使用します:
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.promoCode, {
message: 'Promo code is required for discounts',
when: ({valueOf}) => valueOf(schemaPath.applyDiscount),
});
});
バリデーションルールは、when関数がtrueを返す場合にのみ実行されます。
NOTE: requiredは空の配列に対してtrueを返します。配列のバリデーションにはminLength()を使用してください。
email()
email()バリデーションルールは、有効なメール形式をチェックします:
import {Component, signal} from '@angular/core';
import {form, FormField, email} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField],
template: `
<form novalidate>
<label>
Your Email
<input type="email" [formField]="contactForm.email" />
</label>
</form>
`,
})
export class ContactComponent {
contactModel = signal({email: ''});
contactForm = form(this.contactModel, (schemaPath) => {
email(schemaPath.email, {message: 'Please enter a valid email address'});
});
}
email()バリデーションルールは、標準的なメール形式の正規表現を使用します。user@example.comのようなアドレスは受け入れますが、user@や@example.comのような不正な形式のアドレスは拒否します。
min()とmax()
min()とmax()バリデーションルールは、数値に対して機能します:
import {Component, signal} from '@angular/core';
import {form, FormField, min, max} from '@angular/forms/signals';
@Component({
selector: 'app-age-form',
imports: [FormField],
template: `
<form novalidate>
<label>
Age
<input type="number" [formField]="ageForm.age" />
</label>
<label>
Rating (1-5)
<input type="number" [formField]="ageForm.rating" />
</label>
</form>
`,
})
export class AgeFormComponent {
ageModel = signal({
age: 0,
rating: 0,
});
ageForm = form(this.ageModel, (schemaPath) => {
min(schemaPath.age, 18, {message: 'You must be at least 18 years old'});
max(schemaPath.age, 120, {message: 'Please enter a valid age'});
min(schemaPath.rating, 1, {message: 'Rating must be at least 1'});
max(schemaPath.rating, 5, {message: 'Rating cannot exceed 5'});
});
}
動的な制約のために、算出値を使用できます:
ageForm = form(this.ageModel, (schemaPath) => {
min(schemaPath.participants, () => this.minimumRequired(), {
message: 'Not enough participants',
});
});
minLength()とmaxLength()
minLength()とmaxLength()バリデーションルールは、文字列と配列に対して機能します:
import {Component, signal} from '@angular/core';
import {form, FormField, minLength, maxLength} from '@angular/forms/signals';
@Component({
selector: 'app-password-form',
imports: [FormField],
template: `
<form novalidate>
<label>
Password
<input type="password" [formField]="passwordForm.password" />
</label>
<label>
Bio
<textarea [formField]="passwordForm.bio"></textarea>
</label>
</form>
`,
})
export class PasswordFormComponent {
passwordModel = signal({
password: '',
bio: '',
});
passwordForm = form(this.passwordModel, (schemaPath) => {
minLength(schemaPath.password, 8, {message: 'Password must be at least 8 characters'});
maxLength(schemaPath.password, 100, {message: 'Password is too long'});
maxLength(schemaPath.bio, 500, {message: 'Bio cannot exceed 500 characters'});
});
}
文字列の場合、「length」は文字数を意味します。配列の場合、「length」は要素数を意味します。
pattern()
pattern()バリデーションルールは、正規表現に対してバリデーションを行います:
import {Component, signal} from '@angular/core';
import {form, FormField, pattern} from '@angular/forms/signals';
@Component({
selector: 'app-phone-form',
imports: [FormField],
template: `
<form novalidate>
<label>
Phone Number
<input [formField]="phoneForm.phone" placeholder="555-123-4567" />
</label>
<label>
Postal Code
<input [formField]="phoneForm.postalCode" placeholder="12345" />
</label>
</form>
`,
})
export class PhoneFormComponent {
phoneModel = signal({
phone: '',
postalCode: '',
});
phoneForm = form(this.phoneModel, (schemaPath) => {
pattern(schemaPath.phone, /^\d{3}-\d{3}-\d{4}$/, {
message: 'Phone must be in format: 555-123-4567',
});
pattern(schemaPath.postalCode, /^\d{5}$/, {
message: 'Postal code must be 5 digits',
});
});
}
一般的なパターン:
| パターンの種類 | 正規表現 | 例 |
|---|---|---|
| 電話番号 | /^\d{3}-\d{3}-\d{4}$/ |
555-123-4567 |
| 郵便番号 (米国) | /^\d{5}$/ |
12345 |
| 英数字 | /^[a-zA-Z0-9]+$/ |
abc123 |
| URLセーフ | /^[a-zA-Z0-9_-]+$/ |
my-url_123 |
配列アイテムのバリデーション
フォームには、ネストされたオブジェクトの配列を含めることができます(例: 注文アイテムのリスト)。配列内の各アイテムにバリデーションルールを適用するには、スキーマ関数内でapplyEach()を使用します。applyEach()は配列パスを反復処理し、各アイテムのパスを提供します。このパスでは、トップレベルのフィールドと同様にバリデーターを適用できます。
import {Component, signal} from '@angular/core';
import {applyEach, FormField, form, min, required, SchemaPathTree} from '@angular/forms/signals';
type Item = {name: string; quantity: number};
interface Order {
title: string;
description: string;
items: Item[];
}
function ItemSchema(item: SchemaPathTree<Item>) {
required(item.name, {message: 'Item name is required'});
min(item.quantity, 1, {message: 'Quantity must be at least 1'});
}
@Component(/* ... */)
export class OrderComponent {
orderModel = signal<Order>({
title: '',
description: '',
items: [{name: '', quantity: 0}],
});
orderForm = form(this.orderModel, (schemaPath) => {
required(schemaPath.title);
required(schemaPath.description);
applyEach(schemaPath.items, ItemSchema);
});
}
バリデーションエラー
バリデーションルールが失敗すると、何が問題だったかを説明するエラーオブジェクトが生成されます。エラーの構造を理解することは、ユーザーに明確なフィードバックを提供するのに役立ちます。
エラーの構造
各バリデーションエラーオブジェクトには、以下のプロパティが含まれています:
| プロパティ | 説明 |
|---|---|
kind |
失敗したバリデーションルール(例: "required", "email", "minLength") |
message |
オプションの人間が読める形式のエラーメッセージ |
組み込みのバリデーションルールは、自動的にkindプロパティを設定します。messageプロパティはオプションで、バリデーションルールのオプションを通じてカスタムメッセージを提供できます。
カスタムエラーメッセージ
すべての組み込みバリデーションルールは、カスタムエラーテキストのためにmessageオプションを受け入れます:
import {Component, signal} from '@angular/core';
import {form, FormField, required, minLength} from '@angular/forms/signals';
@Component({
selector: 'app-signup',
imports: [FormField],
template: `
<form novalidate>
<label>
Username
<input [formField]="signupForm.username" />
</label>
<label>
Password
<input type="password" [formField]="signupForm.password" />
</label>
</form>
`,
})
export class SignupComponent {
signupModel = signal({
username: '',
password: '',
});
signupForm = form(this.signupModel, (schemaPath) => {
required(schemaPath.username, {
message: 'Please choose a username',
});
required(schemaPath.password, {
message: 'Password cannot be empty',
});
minLength(schemaPath.password, 12, {
message: 'Password must be at least 12 characters for security',
});
});
}
カスタムメッセージは、明確で具体的であり、ユーザーに問題の修正方法を伝えるべきです。「無効な入力」の代わりに、「セキュリティのため、パスワードは12文字以上である必要があります」のようにします。
フィールドごとの複数のエラー
フィールドに複数のバリデーションルールがある場合、各バリデーションルールは独立して実行され、エラーを生成する可能性があります:
signupForm = form(this.signupModel, (schemaPath) => {
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Enter a valid email address'});
minLength(schemaPath.email, 5, {message: 'Email is too short'});
});
emailフィールドが空の場合、required()エラーのみが表示されます。ユーザーが"a@b"と入力すると、email()とminLength()の両方のエラーが表示されます。すべてのバリデーションルールが実行され、最初の失敗でバリデーションが停止することはありません。
TIP: テンプレートでtouched() && invalid()パターンを使用すると、ユーザーがフィールドを操作する前にエラーが表示されるのを防ぐことができます。バリデーションエラーの表示に関する包括的なガイダンスについては、フィールド状態管理ガイドを参照してください。
カスタムバリデーションルール
組み込みのバリデーションルールは一般的なケースを処理しますが、ビジネスルール、複雑なフォーマット、またはドメイン固有の制約のために、カスタムバリデーションロジックが必要になることがよくあります。
validate()の使用
validate()関数はカスタムバリデーションルールを作成します。これは、フィールドコンテキストにアクセスし、以下の値を返すバリデーター関数を受け取ります:
| 戻り値 | 意味 |
|---|---|
| エラーオブジェクト | 値は無効です |
null または undefined |
値は有効です |
import {Component, signal} from '@angular/core';
import {form, FormField, validate} from '@angular/forms/signals';
@Component({
selector: 'app-url-form',
imports: [FormField],
template: `
<form novalidate>
<label>
Website URL
<input [formField]="urlForm.website" />
</label>
</form>
`,
})
export class UrlFormComponent {
urlModel = signal({website: ''});
urlForm = form(this.urlModel, (schemaPath) => {
validate(schemaPath.website, ({value}) => {
if (!value().startsWith('https://')) {
return {
kind: 'https',
message: 'URL must start with https://',
};
}
return null;
});
});
}
バリデーター関数は、以下のプロパティを持つFieldContextオブジェクトを受け取ります:
| プロパティ | 型 | 説明 |
|---|---|---|
value |
Signal | 現在のフィールド値を含むSignal |
state |
FieldState | フィールドの状態への参照 |
field |
FieldTree | フィールドツリーへの参照 |
valueOf() |
Method | パスで指定された他のフィールドの値を取得します |
stateOf() |
Method | パスで指定された他のフィールドの状態を取得します |
fieldTreeOf() |
Method | パスで指定された他のフィールドのフィールドツリーを取得します |
pathKeys |
Signal | ルートから現在のフィールドまでのパスキー |
NOTE: 子フィールドにはkeyシグナルもあり、配列アイテムのフィールドにはkeyとindexの両方のシグナルがあります。
バリデーションが失敗した場合はkindとmessageを持つエラーオブジェクトを返します。バリデーションが成功した場合はnullまたはundefinedを返します。
validateTree()の使用
validateTree()関数は、複数のフィールドをターゲットにしたり、サブツリー全体に複雑なバリデーションロジックを提供したりできるカスタムバリデーションルールを作成します。
import {Component, model} from '@angular/core';
import {form, FormField, validateTree} from '@angular/forms/signals';
interface User {
firstName: string;
lastName: string;
}
@Component({
/* ... */
})
export class UserFormComponent {
readonly userModel = model<DTO>({
firstName: '',
lastName: '',
});
userForm = form(this.userModel, (path) => {
validateTree(path, (ctx) => {
if (ctx.valueOf(path.firstName).length < 5) {
return {
kind: 'minLength5',
message: 'First name must be at least 5 characters',
fieldTree: ctx.fieldTree.lastName,
};
}
return null;
});
});
}
validateTree()バリデーター関数は、validate()と同じFieldContextオブジェクトを受け取ります。
再利用可能なバリデーションルール
validate()をラップして、再利用可能なバリデーションルール関数を作成します:
function url(path: SchemaPath<string>, options?: {message?: string}) {
validate(path, ({value}) => {
try {
new URL(value());
return null;
} catch {
return {
kind: 'url',
message: options?.message || 'Enter a valid URL',
};
}
});
}
function phoneNumber(path: SchemaPath<string>, options?: {message?: string}) {
validate(path, ({value}) => {
const phoneRegex = /^\d{3}-\d{3}-\d{4}$/;
if (!phoneRegex.test(value())) {
return {
kind: 'phoneNumber',
message: options?.message || 'Phone must be in format: 555-123-4567',
};
}
return null;
});
}
カスタムバリデーションルールは、組み込みのバリデーションルールと同じように使用できます:
urlForm = form(this.urlModel, (schemaPath) => {
url(schemaPath.website, {message: 'Please enter a valid website URL'});
phoneNumber(schemaPath.phone);
});
クロスフィールドバリデーション
クロスフィールドバリデーションは、複数のフィールド値を比較または関連付けます。
クロスフィールドバリデーションの一般的なシナリオは、パスワードの確認です:
import {Component, signal} from '@angular/core';
import {form, FormField, required, minLength, validate} from '@angular/forms/signals';
@Component({
selector: 'app-password-change',
imports: [FormField],
template: `
<form novalidate>
<label>
New Password
<input type="password" [formField]="passwordForm.password" />
</label>
<label>
Confirm Password
<input type="password" [formField]="passwordForm.confirmPassword" />
</label>
<button type="submit">Change Password</button>
</form>
`,
})
export class PasswordChangeComponent {
passwordModel = signal({
password: '',
confirmPassword: '',
});
passwordForm = form(this.passwordModel, (schemaPath) => {
required(schemaPath.password, {message: 'Password is required'});
minLength(schemaPath.password, 8, {message: 'Password must be at least 8 characters'});
required(schemaPath.confirmPassword, {message: 'Please confirm your password'});
validate(schemaPath.confirmPassword, ({value, valueOf}) => {
const confirmPassword = value();
const password = valueOf(schemaPath.password);
if (confirmPassword !== password) {
return {
kind: 'passwordMismatch',
message: 'Passwords do not match',
};
}
return null;
});
});
}
確認用のバリデーションルールはvalueOf(schemaPath.password)を使用してパスワードフィールドの値にアクセスし、確認用の値と比較します。このバリデーションルールはリアクティブに実行されます - どちらかのパスワードが変更されると、バリデーションが自動的に再実行されます。
非同期バリデーション
非同期バリデーションは、サーバーでのユーザー名の利用可能性のチェックやAPIに対するバリデーションなど、外部データソースを必要とするバリデーションを処理します。
validateHttp()の使い方
validateHttp()関数は、HTTPベースのバリデーションを実行します:
import {Component, signal} from '@angular/core';
import {form, FormField, required, validateHttp} from '@angular/forms/signals';
@Component({
selector: 'app-username-form',|
imports: [FormField],
template: `
<form novalidate>
<label>
Username
<input [formField]="usernameForm.username" />
@if (usernameForm.username().pending()) {
<span class="checking">Checking availability...</span>
}
</label>
</form>
`,
})
export class UsernameFormComponent {
usernameModel = signal({username: ''});
usernameForm = form(this.usernameModel, (schemaPath) => {
required(schemaPath.username, {message: 'Username is required'});
validateHttp(schemaPath.username, {
request: ({value}) => `/api/check-username?username=${value()}`,
onSuccess: (response: any) => {
if (response.taken) {
return {
kind: 'usernameTaken',
message: 'Username is already taken',
};
}
return null;
},
onError: (error) => ({
kind: 'networkError',
message: 'Could not verify username availability',
}),
});
});
}
validateHttp()バリデーションルール:
request関数によって返されるURLまたはリクエストを呼び出しますonSuccessを使用して、成功レスポンスをバリデーションエラーまたはnullにマッピングしますonErrorを使用して、リクエストの失敗(ネットワークエラー、HTTPエラー)を処理します- リクエストが進行中の間、
pending()をtrueに設定します - すべての同期バリデーションルールが成功した後にのみ実行されます
ペンディング状態
非同期バリデーションの実行中、フィールドのpending()シグナルはtrueを返します。これを使用してローディングインジケーターを表示します:
@if (form.username().pending()) {
<span class="spinner">Checking...</span>
}
valid()シグナルは、まだエラーがない場合でも、バリデーションがペンディング中の間はfalseを返します。invalid()シグナルは、エラーが存在する場合にのみtrueを返します。
スキーマバリデーションライブラリとの統合
シグナルフォームは、ZodやValibotのようなStandard Schemaに準拠したライブラリに対する組み込みサポートを提供しています。統合はvalidateStandardSchema関数によって提供されます。これにより、シグナルフォームのリアクティブなバリデーションの利点を維持しながら、既存のスキーマを使用できます。
import {form, validateStandardSchema} from '@angular/forms/signals';
import * as z from 'zod';
// スキーマを定義
const userSchema = z.object({
email: z.email(),
password: z.string().min(8),
});
// シグナルフォームで使用
const userForm = form(signal({email: '', password: ''}), (schemaPath) => {
validateStandardSchema(schemaPath, userSchema);
});
ダイナミックスキーマ
依存関係が変更されたときにバリデーションスキーマが自動的に更新されるように、静的なスキーマの代わりにシグナルを渡すことができます。
import {Component, computed, signal} from '@angular/core';
import {form, FormField, validateStandardSchema} from '@angular/forms/signals';
import z from 'zod';
@Component({
/* ... */
})
export class DynamicSchema {
model = signal({document: '', type: 'dni'});
// Schema reacts automatically to type changes
schema = computed(() =>
z.object({
document:
this.model().type === 'dni'
? z.string().length(8, 'DNI must be 8 digits')
: z.string().min(12, 'Passport must be at least 12 characters'),
}),
);
f = form(this.model, (p) => validateStandardSchema(p, () => this.schema()));
}
次のステップ
このガイドでは、バリデーションルールの作成と適用について説明しました。関連ガイドでは、シグナルフォームの他の側面について解説します: