Управление состоянием поля
Состояние поля в Signal Forms позволяет реагировать на взаимодействия пользователя, предоставляя реактивные сигналы для статуса проверки (например, valid, invalid, errors), отслеживания взаимодействия (например, touched, dirty) и доступности (например, disabled, hidden).
Понимание состояния поля
Когда вы создаёте форму с помощью функции 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() | Поле доступно только для чтения и не влияет на состояние родительской формы |
Эти сигналы позволяют создавать адаптивные формы, реагирующие на поведение пользователя. В следующих разделах подробно рассматривается каждая категория.
Состояние проверки
Сигналы состояния проверки сообщают, действительно ли поле и какие ошибки оно содержит.
ПРИМЕЧАНИЕ: В этом руководстве рассматривается использование состояния проверки в шаблонах и логике (например, чтение 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() | В поле есть ошибки проверки |
При проверке валидности в коде используйте invalid() вместо !valid(), если необходимо различать состояния «есть ошибки» и «проверка ожидает завершения». Причина в том, что при выполнении асинхронной проверки valid() и invalid() могут одновременно иметь значение false: поле пока недействительно, поскольку проверка ещё не завершена, но при этом оно также не является невалидным, поскольку ошибки ещё не обнаружены.
Чтение ошибок проверки
Получите массив ошибок проверки с помощью errors(). Каждый объект ошибки содержит:
| Свойство | Описание |
|---|---|
kind | Правило проверки, которое не было выполнено (например, «обязательное поле» или «электронная почта») |
message | Необязательное понятное пользователю сообщение об ошибке |
fieldTree | Ссылка на FieldTree, в котором произошла ошибка |
ПРИМЕЧАНИЕ: Свойство 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() отслеживает, переводил ли пользователь фокус на поле, а затем снимал его. Он принимает значение true, когда пользователь переводит фокус на поле, а затем снимает его в результате взаимодействия (не программно). Скрытые, отключённые поля и поля только для чтения не являются интерактивными и не переходят в состояние касания в результате взаимодействия с пользователем.
Состояние изменения
Часто формам необходимо определять, действительно ли изменились данные, — например, чтобы предупреждать пользователей о несохранённых изменениях или активировать кнопку сохранения только при необходимости. Сигнал 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() для предупреждений о несохранённых изменениях или чтобы активировать кнопки сохранения только при изменении данных.
Состояния касания и изменения
Эти сигналы отслеживают разные виды взаимодействия пользователя:
| Сигнал | Когда принимает значение true |
|---|---|
touched() | Пользователь переводил фокус на интерактивное поле и снимал его (даже если ничего не изменил) |
dirty() | Пользователь изменил интерактивное поле (даже если не снимал с него фокус и даже если текущее значение совпадает с исходным) |
Поле может находиться в разных сочетаниях состояний:
| Состояние | Сценарий |
|---|---|
| Касание есть, изменений нет | Пользователь переводил фокус на поле и снимал его, но ничего не менял |
| Есть и касание, и изменения | Пользователь перевёл фокус на поле, изменил значение и снял фокус |
ПРИМЕЧАНИЕ: Скрытые, отключённые поля и поля только для чтения не являются интерактивными — в результате взаимодействия с пользователем они не переходят в состояние касания или изменения.
Состояние доступности
Сигналы состояния доступности управляют тем, являются ли поля интерактивными, доступными для редактирования или видимыми. Отключённые и скрытые поля, а также поля только для чтения не являются интерактивными. Они не влияют на валидность, состояние касания или состояние изменений родительской формы.
Отключённые поля
Сигнал disabled() указывает, принимает ли поле пользовательский ввод. Отключённые поля отображаются в интерфейсе, но пользователи не могут с ними взаимодействовать.
import {Component, signal} from '@angular/core';
import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-order',
imports: [FormField],
// TIP: The `[formField]` directive automatically binds the `disabled` attribute based
// on the field's `disabled()` state, so you don't need to manually add `[disabled]="field().disabled()"`
template: `
<input [formField]="orderForm.couponCode" />
@if (orderForm.couponCode().disabled()) {
<p class="info">Coupon code is only available for orders over $50</p>
}
`,
})
export class Order {
orderModel = signal({
total: 25,
couponCode: '',
});
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.couponCode, {when: ({valueOf}) => valueOf(schemaPath.total) < 50});
});
}
В этом примере мы используем valueOf(schemaPath.total) для проверки значения поля total, чтобы определить, следует ли отключить couponCode.
ПРИМЕЧАНИЕ: Параметр обратного вызова схемы (schemaPath в этих примерах) — это объект SchemaPathTree, предоставляющий пути ко всем полям формы. Вы можете назвать этот параметр как угодно.
При определении правил, таких как disabled(), hidden() или readonly(), функция when получает объект FieldContext, который обычно деструктурируют (например, ({valueOf})). В правилах проверки часто используются два метода:
-
valueOf(schemaPath.otherField)— чтение значения другого поля формы -
value()— сигнал, содержащий значение поля, к которому применяется правило
Отключённые поля не участвуют в проверке родительской формы. Даже если отключённое поле было бы невалидным, родительская форма всё равно может быть валидной. Состояние disabled() влияет на интерактивность и проверку, но не изменяет значение поля.
Скрытые поля
Сигнал hidden() указывает, скрыто ли поле условно. Используйте hidden() вместе с @if, чтобы показывать или скрывать поля в зависимости от условий:
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" />
Make profile public
</label>
@if (!profileForm.publicUrl().hidden()) {
<label>
Public URL
<input [formField]="profileForm.publicUrl" />
</label>
}
`,
})
export class Profile {
profileModel = signal({
isPublic: false,
publicUrl: '',
});
profileForm = form(this.profileModel, (schemaPath) => {
hidden(schemaPath.publicUrl, {when: ({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>
Username (cannot be changed)
<input [formField]="accountForm.username" />
</label>
<label>
Email
<input [formField]="accountForm.email" />
</label>
`,
})
export class Account {
accountModel = signal({
username: 'johndoe',
email: 'john@example.com',
});
accountForm = form(this.accountModel, (schemaPath) => {
readonly(schemaPath.username);
});
}
ПРИМЕЧАНИЕ: Директива [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() | По крайней мере в одном интерактивном поле есть ошибки проверки |
pending() | По крайней мере для одного интерактивного поля выполняется асинхронная проверка |
touched() | Пользователь коснулся по крайней мере одного интерактивного поля |
dirty() | Пользователь изменил по крайней мере одно интерактивное поле |
Когда использовать состояние на уровне формы, а когда — на уровне поля
Используйте состояние на уровне формы для:
- Определения, активна ли кнопка отправки
- Определения состояния кнопки «Сохранить»
- Проверки валидности формы в целом
- Предупреждений о несохранённых изменениях
Используйте состояние на уровне поля для:
- Сообщений об ошибках для отдельных полей
- Стилизации отдельных полей
- Отображения результатов проверки для каждого поля
- Условного управления доступностью полей
Распространение состояния
Состояние поля распространяется от дочерних полей вверх через родительские группы полей к корневой форме.
Как состояние дочернего поля влияет на родительские формы
Когда дочернее поле становится невалидным, его родительская группа полей также становится невалидной, как и корневая форма. Когда дочернее поле переходит в состояние касания или изменения, родительская группа полей и корневая форма отражают это изменение. Такое агрегирование позволяет проверять валидность на любом уровне — отдельного поля или всей формы.
const userModel = signal({
profile: {
firstName: '',
lastName: '',
},
address: {
street: '',
city: '',
},
});
const userForm = form(userModel);
// If firstName is invalid, profile is invalid
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, {when: ({valueOf}) => !valueOf(schemaPath.requiresShipping)});
});
В этом примере, когда shippingAddress скрыто, оно не влияет на валидность формы. Поэтому форма может быть валидной, даже если shippingAddress пусто и является обязательным.
Такое поведение не позволяет скрытым и отключённым полям, а также полям только для чтения блокировать отправку формы или влиять на проверку, состояние касания и состояние изменений.
Использование состояния в шаблонах
Сигналы состояния поля легко интегрируются с шаблонами 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, {
when: ({valueOf}) => !valueOf(schemaPath.requiresShipping),
});
});
}
Скрытые поля не участвуют в проверке, поэтому форму можно отправить, даже если скрытое поле в противном случае было бы невалидным.
Отслеживание значений полей массива
В Signal Forms блок @for, перебирающий набор полей, следует отслеживать по идентификатору поля.
@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);
}
Система форм уже отслеживает значения модели в массиве и автоматически поддерживает стабильную идентичность создаваемых полей.
Когда элемент изменяется, он может представлять новую логическую сущность, даже если некоторые его свойства выглядят одинаково. Отслеживание по идентификатору гарантирует, что фреймворк будет считать его отдельным элементом, а не повторно использовать существующие элементы интерфейса. Это предотвращает ошибочное совместное использование элементов с состоянием, например полей ввода, и сохраняет соответствие привязок нужной части модели.
Использование состояния поля в логике компонента
Сигналы состояния поля работают с реактивными примитивами 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
Создавайте вычисляемые сигналы на основе состояния поля, чтобы они автоматически обновлялись при изменении этого состояния:
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
);
});
} Программное изменение состояния
Хотя состояние поля обычно обновляется в результате взаимодействия с пользователем (ввод текста, перевод фокуса на поле и его снятие), иногда требуется управлять им программно. К типичным сценариям относятся отправка и сброс формы.
Отправка формы
Signal Forms предоставляет директиву FormRoot, упрощающую отправку формы. Она автоматически отменяет поведение браузера по умолчанию при отправке формы и задаёт атрибут novalidate для элемента <form>.
import {FormField, FormRoot} from '@angular/forms/signals';
@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
}
}
При использовании FormRoot отправка формы автоматически вызывает функцию submit(), которая переводит все поля в состояние касания (чтобы показать ошибки проверки) и выполняет обратный вызов action, если форма валидна.
Форму также можно отправить вручную, не используя директиву, вызвав submit(this.registrationForm). При явном вызове функции submit таким образом можно передать объект FormSubmitOptions, чтобы переопределить логику submission формы по умолчанию: submit(this.registrationForm, {action: () => /* ... */ }).
Сброс формы после отправки
После успешной отправки формы может потребоваться вернуть её в исходное состояние, очистив историю взаимодействия пользователя и значения полей. Метод reset() сбрасывает флаги касания и изменения. В reset() также можно передать необязательное значение, чтобы обновить данные модели:
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});
},
},
});
}
Это позволяет подготовить форму к новому вводу, не отображая устаревшие сообщения об ошибках или индикаторы состояния изменений.
Стилизация на основе состояния проверки
Вы можете применять пользовательские стили к форме, привязывая CSS-классы к состоянию проверки:
import {Component, signal} from '@angular/core';
import {form, FormField, email} from '@angular/forms/signals';
@Component({
imports: [FormField],
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() гарантирует, что стили появятся только после того, как пользователь взаимодействовал с полем.
Дальнейшие шаги
В этом руководстве рассмотрены обработка состояния проверки и доступности, отслеживание взаимодействия и распространение состояния поля. В связанных руководствах рассматриваются другие аспекты Signal Forms:
Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/forms/signals/field-state-management