Добавление логики формы
Signal Forms позволяют добавлять логику в форму с помощью схем. Логика валидации описана в руководстве по валидации, а в этом руководстве рассматриваются другие правила, доступные в схемах. Можно условно отключать поля, скрывать их в зависимости от других значений, делать их доступными только для чтения, добавлять задержку для пользовательского ввода и прикреплять метаданные для пользовательских элементов управления.
В этом руководстве показано, как использовать такие правила, как disabled(), hidden(), readonly(), debounce() и metadata(), чтобы управлять поведением полей.
Когда добавлять логику формы
Используйте правила, если поведение поля зависит от значений других полей или должно обновляться реактивно. Например:
- Поле для ввода купона, отключенное, если сумма заказа слишком мала
- Поле адреса, скрытое, если доставка не требуется
- Поле поиска с задержкой ввода для сокращения количества запросов к API
Как работают правила
Правила связывают реактивную логику с определенными полями формы. Большинство условных правил принимают объект параметров с функцией when. Функция when автоматически пересчитывается при каждом изменении сигналов, на которые она ссылается, так же как computed.
const orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.couponCode, {when: ({valueOf}) => valueOf(schemaPath.total) < 50});
//~~~~~~ ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//rule path reactive logic function
});
Функции реактивной логики получают объект FieldContext, который предоставляет доступ к значениям и состоянию полей с помощью вспомогательных функций, таких как valueOf() и stateOf(). Часто объект деструктурируют, чтобы обращаться к этим функциям напрямую.
ПРИМЕЧАНИЕ: Параметр обратного вызова схемы (schemaPath в этих примерах) — это объект SchemaPathTree, предоставляющий пути ко всем полям формы. Этому параметру можно дать любое имя.
Полное описание свойств и методов FieldContext см. в руководстве по валидации.
Запрет обновления полей с помощью disabled()
Правило disabled() задает состояние отключения поля.
Оно работает с директивой [formField], которая автоматически привязывает атрибут disabled в зависимости от состояния поля, поэтому добавлять [disabled]="yourForm.fieldName().disabled()" в шаблон вручную не нужно.
ПРИМЕЧАНИЕ: Отключенные поля пропускают валидацию — они не участвуют в проверках формы. Значение поля сохраняется, но не проверяется. Подробнее о поведении валидации см. в руководстве по валидации.
Всегда отключено
Чтобы отключить поле навсегда, вызовите disabled(), указав только путь к полю:
import {Component, signal} from '@angular/core';
import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-settings',
imports: [FormField],
template: `
<label>
System ID (cannot be changed)
<input [formField]="settingsForm.systemId" />
</label>
`,
})
export class Settings {
settingsModel = signal({
systemId: 'SYS-12345',
userName: '',
});
settingsForm = form(this.settingsModel, (schemaPath) => {
disabled(schemaPath.systemId);
});
} Условное отключение
Чтобы отключать поле при определенных условиях, передайте функцию when, которая возвращает true (отключено) или false (включено):
import {Component, signal} from '@angular/core';
import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-order',
imports: [FormField],
template: `
<label>
Order Total
<input type="number" [formField]="orderForm.total" />
</label>
<label>
Coupon Code
<input [formField]="orderForm.couponCode" />
</label>
`,
})
export class Order {
orderModel = signal({
total: 25,
couponCode: '',
});
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.couponCode, {when: ({valueOf}) => valueOf(schemaPath.total) < 50});
});
}
В этом примере поле для ввода купона отключается, если сумма заказа меньше 50 долларов.
Причины отключения
При отключении поля укажите понятное пользователю объяснение: вместо true верните строку:
import {Component, signal} from '@angular/core';
import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-order',
imports: [FormField],
template: `
<label>
Order Total
<input type="number" [formField]="orderForm.total" />
</label>
<label>
Coupon Code
<input [formField]="orderForm.couponCode" />
</label>
@if (orderForm.couponCode().disabled()) {
<div class="info">
@for (reason of orderForm.couponCode().disabledReasons(); track reason) {
<p>{{ reason.message }}</p>
}
</div>
}
`,
})
export class Order {
orderModel = signal({
total: 25,
couponCode: '',
});
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.couponCode, {
when: ({valueOf}) =>
valueOf(schemaPath.total) < 50 ? 'Order must be $50 or more to use a coupon' : false,
});
});
}
Функция when возвращает:
- Строку, чтобы отключить поле и указать причину
-
false, чтобы включить поле (не любое ложное значение, а именноfalse)
Получить причины можно через сигнал disabledReasons() в состоянии поля. Каждая причина содержит свойство message со строкой, которую вы вернули.
Несколько причин отключения
Можно также несколько раз вызвать disabled() для одного поля; все возвращенные причины накапливаются:
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.promoCode, {
when: ({valueOf}) =>
!valueOf(schemaPath.hasAccount) ? 'You must have an account to use promo codes' : false,
});
disabled(schemaPath.promoCode, {
when: ({valueOf}) => (valueOf(schemaPath.total) < 25 ? 'Order must be at least $25' : false),
});
});
Если истинны оба условия, поле отображает обе причины отключения. Такой подход полезен для сложных правил доступности, которые нужно разделить.
Настройка состояния hidden() для полей
Правило hidden() задает состояние скрытия поля. Однако оно только устанавливает программное состояние. Вы сами определяете, будет ли поле отображаться в интерфейсе.
ВАЖНО: В отличие от disabled и readonly, для состояния hidden нет собственного свойства DOM. Директива [formField] не добавляет элементам атрибут hidden. Чтобы условно отображать поля в зависимости от состояния hidden(), используйте в шаблоне @if или CSS.
ПРИМЕЧАНИЕ: Как и отключенные поля, скрытые поля пропускают валидацию. Подробнее см. в руководстве по валидации.
Основы скрытия полей
Используйте hidden() с функцией when, которая возвращает true (скрыто) или false (видимо):
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)});
});
} Отображение нередактируемых полей с помощью readonly()
Правило readonly() запрещает пользователям изменять поле. Директива [FormField] автоматически связывает это состояние с HTML-атрибутом readonly, который запрещает редактирование, но позволяет устанавливать фокус и выделять текст.
ПРИМЕЧАНИЕ: Поля, доступные только для чтения, пропускают валидацию.
Всегда доступно только для чтения
Чтобы навсегда сделать поле доступным только для чтения, вызовите 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 в зависимости от состояния поля.
Условный режим «только для чтения»
Чтобы сделать поле доступным только для чтения при определенных условиях, передайте функцию when:
import {Component, signal} from '@angular/core';
import {form, FormField, readonly} from '@angular/forms/signals';
@Component({
selector: 'app-document',
imports: [FormField],
template: `
<label>
<input type="checkbox" [formField]="documentForm.isLocked" />
Lock document
</label>
<label>
Document Title
<input [formField]="documentForm.title" />
</label>
`,
})
export class Document {
documentModel = signal({
isLocked: false,
title: 'Untitled',
});
documentForm = form(this.documentModel, (schemaPath) => {
readonly(schemaPath.title, {when: ({valueOf}) => valueOf(schemaPath.isLocked)});
});
}
Когда isLocked имеет значение true, поле заголовка становится доступным только для чтения.
Выбор между hidden, disabled и readonly
Эти три функции конфигурации по-разному управляют доступностью полей:
Выберите hidden(), если поле:
- Не должно отображаться в интерфейсе
- Не относится к текущему состоянию формы
- Пример: поля адреса доставки, если установлен флажок «Совпадает с адресом для выставления счета»
Выберите disabled(), если поле:
- Должно быть видимым, но недоступным для редактирования
- Должно объяснять, почему оно недоступно (с помощью причин отключения)
- Не должно включаться в отправляемую HTML-форму
- Пример: кнопка отправки, отключенная до тех пор, пока форма не станет валидной; поля подтверждения, отключенные для пользователей без прав администратора
Выберите readonly(), если поле:
- Должно быть видимым, но недоступным для редактирования
- Содержит данные, которые пользователь должен видеть, выделять или копировать
- Должно включаться в отправляемую HTML-форму
- Пример: номер подтверждения заказа, системные справочные коды
Все три состояния пропускают валидацию и запрещают пользователю редактировать поле, пока они активны. Основные различия:
| Свойство | hidden() | disabled() | readonly() |
|---|---|---|---|
| Видно в интерфейсе | Нет | Да | Да |
| Можно установить фокус/выделить текст | Нет | Нет | Да |
| Включается в отправляемую HTML-форму | Нет | Нет | Да |
Задержка обработки ввода с помощью debounce()
Правило debounce() откладывает обновление модели формы. Это полезно для оптимизации производительности и сокращения количества лишних операций при быстром вводе.
Как работает задержка ввода
Без задержки каждый нажатый символ немедленно обновляет модель формы. Это может запускать:
- Ресурсоемкие вычисляемые сигналы, пересчитываемые при каждом изменении
- Проверки валидации после каждого символа
- Запросы к API и другие побочные эффекты, связанные со значением модели
Задержка откладывает эти обновления и сокращает ненужную работу.
Основы задержки ввода
Задержку для поля можно задать в миллисекундах:
import {Component, signal} from '@angular/core';
import {form, FormField, debounce} from '@angular/forms/signals';
@Component({
selector: 'app-search',
imports: [FormField],
template: `
<label>
Search
<input [formField]="searchForm.query" />
</label>
<p>Searching for: {{ searchForm.query().value() }}</p>
`,
})
export class Search {
searchModel = signal({
query: '',
});
searchForm = form(this.searchModel, (schemaPath) => {
debounce(schemaPath.query, 300);
});
}
При задержке в 300 мс:
- Пользователь вводит текст в поле
- Модель формы обновляется только после 300 мс бездействия при вводе
- Если пользователь продолжает вводить текст, таймер сбрасывается после каждого нажатия клавиши
- Когда пользователь приостанавливает ввод на 300 мс, модель обновляется окончательным значением
Гарантии синхронизации
Функция debounce() не позволяет потерять введенные пользователем данные благодаря следующим механизмам:
- При пометке поля как затронутого: Значение синхронизируется немедленно, а ожидающая задержка отменяется. Это происходит, когда поле теряет фокус (событие blur) или явно помечается как затронутое.
- При отправке формы: Перед валидацией все поля помечаются как затронутые, благодаря чему все значения с отложенным обновлением синхронизируются немедленно.
Это позволяет пользователям быстро вводить текст, переходить к следующему полю или отправлять форму, не дожидаясь окончания задержки.
Пользовательская логика задержки
Для более точного управления передайте функцию задержки, которая определяет, когда нужно синхронизировать значение. Эта функция вызывается при каждом обновлении значения элемента управления и может вернуть undefined для немедленной синхронизации или Promise, который откладывает синхронизацию до своего разрешения:
import {Component, signal} from '@angular/core';
import {form, FormField, debounce} from '@angular/forms/signals';
@Component({
selector: 'app-search',
imports: [FormField],
template: `
<label>
Search
<input [formField]="searchForm.query" />
</label>
`,
})
export class Search {
searchModel = signal({
query: '',
});
searchForm = form(this.searchModel, (schemaPath) => {
debounce(schemaPath.query, () => {
// Return a promise that resolves after 500ms
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), 500);
});
});
});
}
Функция задержки может возвращать:
-
undefinedдля немедленной синхронизации значения Promise<void>, который откладывает синхронизацию до своего разрешения
Пользовательская логика задержки пригодится для следующих задач:
- Реализация собственной логики времени, выходящей за рамки простой задержки
- Координация синхронизации с внешними событиями
- Условная задержка в зависимости от состояния приложения
Когда использовать задержку ввода
Задержка ввода наиболее полезна, если:
- У вас есть ресурсоемкие вычисляемые сигналы, зависящие от значения поля
- Поле запускает запросы к API или другие побочные эффекты
- Вы хотите сократить нагрузку на валидацию при быстром вводе
- Профилирование производительности показывает, что обновления модели вызывают замедления
Не используйте задержку ввода, если:
- Для удобства пользователя поле должно обновляться немедленно (например, поле калькулятора)
- Выигрыш в производительности незначителен
- Пользователи ожидают обратной связи в реальном времени
Связывание данных с полем с помощью metadata()
Метаданные прикрепляют к полю реактивные данные. Правила валидации используют эту систему внутри библиотеки, а вы можете публиковать собственные ключи для специфичной для приложения информации, например справочного текста, конфигурации или вычисляемых отображаемых значений.
Signal Forms предоставляет шесть предопределенных ключей метаданных, которые автоматически заполняются встроенными валидаторами:
| Ключ | Заполняется с помощью | Читается через |
|---|---|---|
REQUIRED | required() | field().required() |
MIN | min() | field().min() |
MAX | max() | field().max() |
MIN_LENGTH | minLength() | field().minLength() |
MAX_LENGTH | maxLength() | field().maxLength() |
PATTERN | pattern() | field().pattern() |
Директива [formField] автоматически привязывает пять из этих ключей (REQUIRED, MIN, MAX, MIN_LENGTH и MAX_LENGTH) к соответствующим HTML-атрибутам стандартного элемента формы. Исключение — PATTERN: Signal Forms поддерживает несколько шаблонов для одного поля, тогда как HTML-атрибут pattern принимает только одно регулярное выражение.
import {Component, signal} from '@angular/core';
import {form, FormField, required, min, max} from '@angular/forms/signals';
@Component({
selector: 'app-age',
imports: [FormField],
template: `
<label>
Age (between {{ ageForm.age().min?.() }} and {{ ageForm.age().max?.() }})
<input type="number" [formField]="ageForm.age" />
</label>
@if (ageForm.age().required()) {
<span class="required-indicator">*</span>
}
`,
})
export class Age {
ageModel = signal({age: 0});
ageForm = form(this.ageModel, (schemaPath) => {
required(schemaPath.age);
min(schemaPath.age, 18);
max(schemaPath.age, 120);
});
} Реактивные метаданные
Правила валидации могут получать ограничения из других полей, благодаря чему опубликованные метаданные становятся реактивными:
import {Component, signal} from '@angular/core';
import {form, FormField, max} from '@angular/forms/signals';
@Component({
selector: 'app-inventory',
imports: [FormField],
template: `
<label>
Item
<select [formField]="inventoryForm.item">
<option value="widget">Widget</option>
<option value="gadget">Gadget</option>
</select>
</label>
<label>
Quantity (max: {{ inventoryForm.quantity().max?.() }})
<input type="number" [formField]="inventoryForm.quantity" />
</label>
`,
})
export class Inventory {
inventoryModel = signal({
item: 'widget',
quantity: 0,
});
inventoryForm = form(this.inventoryModel, (schemaPath) => {
max(schemaPath.quantity, ({valueOf}) => {
const item = valueOf(schemaPath.item);
return item === 'widget' ? 100 : 50;
});
});
}
Правило валидации max() реактивно задает метаданные MAX на основе выбранного элемента, поэтому любой шаблон или элемент управления, считывающий field().max(), обновляется при каждом изменении элемента.
Подробнее о том, как определять пользовательские ключи, объединять значения с помощью редьюсеров и использовать управляемые метаданные для объектов с учетом жизненного цикла, см. в руководстве по метаданным полей.
Объединение правил
К одному полю можно применить несколько правил, а с помощью условной логики — применять целые группы правил в зависимости от состояния формы.
Несколько правил для одного поля
Примените несколько правил, чтобы настроить все аспекты поведения поля:
import {Component, signal} from '@angular/core';
import {form, FormField, disabled, hidden, debounce, metadata} from '@angular/forms/signals';
import {PLACEHOLDER} from './metadata-keys';
@Component({
selector: 'app-promo',
imports: [FormField],
template: `
@if (!promoForm.promoCode().hidden()) {
<label>
Promo Code
<input [formField]="promoForm.promoCode" />
</label>
}
`,
})
export class Promo {
promoModel = signal({
hasAccount: false,
subscriptionType: 'free' as 'free' | 'premium',
promoCode: '',
});
promoForm = form(this.promoModel, (schemaPath) => {
disabled(schemaPath.promoCode, {
when: ({valueOf}) => (!valueOf(schemaPath.hasAccount) ? 'You must have an account' : false),
});
hidden(schemaPath.promoCode, {
when: ({valueOf}) => valueOf(schemaPath.subscriptionType) === 'free',
});
debounce(schemaPath.promoCode, 300);
metadata(schemaPath.promoCode, PLACEHOLDER, () => 'Enter promo code');
});
}
Эти правила работают вместе:
- Скрытие имеет приоритет: если поле скрыто, его состояние отключения не имеет значения
- Отключение запрещает редактирование независимо от состояния «только для чтения»
- Задержка ввода влияет на обновления модели независимо от других состояний
- Метаданные независимы и всегда доступны
Условная логика с applyWhen
Используйте applyWhen(), чтобы условно применять целые группы правил:
import {Component, signal} from '@angular/core';
import {form, FormField, applyWhen, required, pattern} from '@angular/forms/signals';
@Component({
selector: 'app-address',
imports: [FormField],
template: `
<label>
Country
<select [formField]="addressForm.country">
<option value="US">United States</option>
<option value="CA">Canada</option>
</select>
</label>
<label>
Zip/Postal Code
<input [formField]="addressForm.zipCode" />
</label>
`,
})
export class Address {
addressModel = signal({
country: 'US',
zipCode: '',
});
addressForm = form(this.addressModel, (schemaPath) => {
applyWhen(
schemaPath,
({valueOf}) => valueOf(schemaPath.country) === 'US',
(schemaPath) => {
// Only applied when country is US
required(schemaPath.zipCode);
pattern(schemaPath.zipCode, /^\d{5}(-\d{4})?$/);
},
);
});
}
Функция applyWhen() получает:
- Путь, к которому нужно применить логику (часто это путь к корневой форме)
- Функцию реактивной логики, которая возвращает
true(применить) илиfalse(не применять) - Функцию схемы, определяющую условные правила
Условные правила выполняются, только если условие истинно. Это полезно для сложных форм, в которых правила валидации или поведение меняются в зависимости от выбора пользователя.
Повторно используемые функции схемы
Вынесите общие конфигурации правил в повторно используемые функции:
import {SchemaPath, debounce, metadata, maxLength} from '@angular/forms/signals';
import {PLACEHOLDER} from './metadata-keys';
function emailFieldConfig(path: SchemaPath<string>) {
debounce(path, 300);
metadata(path, PLACEHOLDER, () => 'user@example.com');
maxLength(path, 255);
}
// Use in multiple forms
const contactForm = form(contactModel, (schemaPath) => {
emailFieldConfig(schemaPath.email);
emailFieldConfig(schemaPath.alternateEmail);
});
const registrationForm = form(registrationModel, (schemaPath) => {
emailFieldConfig(schemaPath.email);
});
Этот подход полезен, если в приложении есть стандартные конфигурации полей, которые используются в нескольких формах.
Дальнейшие шаги
Дополнительную информацию о 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/form-logic