Validation
Formها برای اینکه مطمئن شوند کاربران قبل از submission، data درست و کامل فراهم میکنند به validation نیاز دارند. بدون validation، باید مشکلهای کیفیت data را در server مدیریت کنید، user experience ضعیفی با error messageهای نامشخص ارائه دهید و هر constraint را دستی بررسی کنید.
Signal Forms یک رویکرد validation مبتنی بر schema فراهم میکند. Validation ruleها با استفاده از schema function به fieldها bind میشوند، هنگام تغییر valueها بهصورت خودکار اجرا میشوند و errorها را از طریق field state signalها expose میکنند. این کار validation reactive را ممکن میکند که همزمان با تعامل کاربران با form update میشود.
/**
* @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 {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],
})
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);
},
});
}
}<form (submit)="onSubmit($event)">
<div>
<label>
Email:
<input type="email" [formField]="loginForm.email" />
</label>
@if (loginForm.email().touched() && loginForm.email().invalid()) {
<ul class="error-list">
@for (error of loginForm.email().errors(); track error) {
<li>{{ error.message }}</li>
}
</ul>
}
</div>
<div>
<label>
Password:
<input type="password" [formField]="loginForm.password" />
</label>
@if (loginForm.password().touched() && loginForm.password().invalid()) {
<ul class="error-list">
@for (error of loginForm.password().errors(); track error) {
<li>{{ error.message }}</li>
}
</ul>
}
</div>
<button type="submit" [disabled]="loginForm().invalid()">Log In</button>
</form>form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 400px;
padding: 1rem;
font-family:
Inter,
system-ui,
-apple-system,
sans-serif;
}
div {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-weight: 500;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
}
input:focus {
outline: none;
border-color: #4285f4;
}
button {
padding: 0.75rem 1.5rem;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #357ae8;
}
button:active {
background-color: #2a65c8;
}
.error-list {
color: red;
font-size: 0.875rem;
margin: 0.25rem 0 0 0;
padding-left: 0;
list-style-position: inside;
}
.error-list li {
margin: 0;
}مبانی Validation
Validation در Signal Forms از طریق schema functionای تعریف میشود که بهعنوان argument دوم به form() پاس داده میشود.
Schema function
Schema function یک object از نوع SchemaPathTree دریافت میکند که به شما اجازه میدهد validation ruleهای خود را تعریف کنید:
/**
* @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 {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],
})
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);
},
});
}
}Schema function هنگام initialization فرم یک بار اجرا میشود. Validation ruleها با استفاده از schema path parameter، مثل schemaPath.email و schemaPath.password، به fieldها bind میشوند و validation هر زمان field valueها تغییر کنند بهصورت خودکار اجرا میشود.
Validation چطور کار میکند
Validation در Signal Forms از این pattern پیروی میکند:
- تعریف validation ruleها در schema - Validation ruleها را در schema function به fieldها bind کنید
- اجرای خودکار - Validation ruleها هنگام تغییر field valueها اجرا میشوند
- Error propagation - Validation errorها از طریق field state signalها expose میشوند
- Updateهای reactive - UI هنگام تغییر validation state بهصورت خودکار update میشود
Validation روی هر value change برای interactive fieldها اجرا میشود. Fieldهای hidden و disabled validation را اجرا نمیکنند؛ validation ruleهای آنها تا زمانی که field دوباره interactive شود skip میشوند.
زمانبندی validation
Validation ruleها به این ترتیب اجرا میشوند:
- Synchronous validation - همه synchronous validation ruleها هنگام تغییر value اجرا میشوند
- Asynchronous validation - Asynchronous validation ruleها فقط بعد از pass شدن همه synchronous validation ruleها اجرا میشوند
- Update شدن field state - Signalهای
valid()،invalid()،errors()وpending()update میشوند
Synchronous validation ruleها مثل required() و email() بلافاصله کامل میشوند. Asynchronous validation ruleها مثل validateHttp() ممکن است زمان ببرند و هنگام اجرا signal مربوط به pending() را روی true بگذارند.
همه validation ruleها روی هر change اجرا میشوند؛ validation بعد از اولین error short-circuit نمیشود. اگر یک field هم required() و هم email() داشته باشد، هر دو اجرا میشوند و هر دو میتوانند همزمان error تولید کنند.
Validation ruleهای built-in
Signal Forms برای سناریوهای validation رایج ruleهایی فراهم میکند. همه validation ruleهای built-in یک options object برای custom error messageها و conditional logic میپذیرند.
required()
Validation rule مربوط به required() مطمئن میشود field value دارد:
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'});
});
}یک field وقتی "empty" در نظر گرفته میشود که:
| Condition | Example |
|---|---|
Value برابر null باشد | null, |
| Value یک string خالی باشد | '' |
برای requirementهای شرطی، از option مربوط به when استفاده کنید:
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.promoCode, {
message: 'Promo code is required for discounts',
when: ({valueOf}) => valueOf(schemaPath.applyDiscount),
});
});Validation rule فقط وقتی اجرا میشود که function مربوط به when مقدار true برگرداند.
Note: required یک array خالی را present یعنی valid در نظر میگیرد، پس برای enforce کردن حداقل تعداد itemهای array از minLength() استفاده کنید؛ همچنین false را missing یعنی invalid در نظر میگیرد، مطابق با <input type="checkbox" required>.
email()
Validation rule مربوط به 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'});
});
}Validation rule مربوط به email() از regex استاندارد email format استفاده میکند. Addressهایی مثل user@example.com را میپذیرد اما addressهای malformed مثل user@ یا @example.com را رد میکند.
min() و max()
Validation ruleهای min() و max() با valueهای numeric کار میکنند:
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'});
});
}میتوانید برای constraintهای dynamic از computed valueها استفاده کنید:
ageForm = form(this.ageModel, (schemaPath) => {
min(schemaPath.participants, () => this.minimumRequired(), {
message: 'Not enough participants',
});
});minLength() و maxLength()
Validation ruleهای minLength() و maxLength() با stringها و arrayها کار میکنند:
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'});
});
}برای stringها، "length" یعنی تعداد characterها. برای arrayها، "length" یعنی تعداد elementها.
pattern()
Validation rule مربوط به pattern() value را با یک regular expression validate میکند:
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',
});
});
}Patternهای رایج:
| Pattern Type | Regular Expression | Example |
|---|---|---|
| Phone | /^\d{3}-\d{3}-\d{4}$/ | 555-123-4567 |
| Postal code (US) | /^\d{5}$/ | 12345 |
| Alphanumeric | /^[a-zA-Z0-9]+$/ | abc123 |
| URL-safe | /^[a-zA-Z0-9_-]+$/ | my-url_123 |
Validation برای array itemها
Formها میتوانند شامل arrayهایی از objectهای nested باشند، مثلا listای از order itemها. برای اعمال validation ruleها روی هر item در یک array، داخل schema function خود از applyEach() استفاده کنید. applyEach()، array path را iterate میکند و برای هر item یک path فراهم میکند که میتوانید validatorها را درست مثل fieldهای top-level روی آن اعمال کنید.
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);
});
}Validation errorها
وقتی validation ruleها fail میشوند، error objectهایی تولید میکنند که توضیح میدهند چه چیزی اشتباه شده است. درک ساختار error کمک میکند feedback روشنی به کاربران بدهید.