Пользовательские элементы управления
ПРИМЕЧАНИЕ: Это руководство предполагает знакомство с основами Signal Forms.
Встроенные в браузер элементы управления формами (например, input, select, textarea) подходят для распространённых случаев, но приложениям часто требуются специализированные поля ввода. Для выбора даты с календарём, редактора форматированного текста с панелью инструментов или выбора тегов с автодополнением нужны пользовательские реализации.
Signal Forms работает с любым компонентом, реализующим определённые интерфейсы. Интерфейс элемента управления определяет свойства и сигналы, позволяющие компоненту взаимодействовать с системой форм. Когда компонент реализует один из этих интерфейсов, директива [formField] автоматически связывает элемент управления с состоянием формы, проверкой и привязкой данных.
Создание простого пользовательского элемента управления
Начнём с минимальной реализации и добавим нужные функции.
Минимальный элемент управления для ввода
Для простого пользовательского элемента ввода достаточно реализовать интерфейс 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.value)"
placeholder="Enter text..."
/>
</div>
`,
})
export class BasicInput implements FormValueControl<string> {
/** The current input value */
value = model('');
} Минимальный элемент управления-флажок
Для элемента управления в виде флажка нужно сделать две вещи:
- Реализовать интерфейс
FormCheckboxControl, чтобы директиваFormFieldраспознала его как элемент управления формы - Предоставить сигнал модели
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>
`,
})
export class BasicToggle implements FormCheckboxControl {
/** Whether the toggle is checked */
checked = model<boolean>(false);
toggle() {
this.checked.update((val) => !val);
}
} Использование пользовательского элемента управления
Создав элемент управления, вы можете использовать его везде, где использовали бы встроенное поле ввода. Для этого добавьте к нему директиву FormField:
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, FormField, required} from '@angular/forms/signals';
import {BasicInput} from './basic-input';
import {BasicToggle} from './basic-toggle';
@Component({
imports: [FormField, BasicInput, BasicToggle],
template: `
<form novalidate>
<label>
Email
<app-basic-input [formField]="registrationForm.email" />
</label>
<label>
Accept terms
<app-basic-toggle [formField]="registrationForm.acceptTerms" />
</label>
<button type="submit" [disabled]="registrationForm().invalid()">Register</button>
</form>
`,
})
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'});
});
}
ПРИМЕЧАНИЕ: Параметр функции обратного вызова схемы (schemaPath в этих примерах) — это объект SchemaPathTree, предоставляющий пути ко всем полям формы. Вы можете назвать этот параметр как угодно.
Директива [formField] работает одинаково как с пользовательскими элементами управления, так и со встроенными полями ввода. Signal Forms обрабатывает их одинаково: проверка выполняется, состояние обновляется, а привязка данных работает автоматически.
Знакомство с интерфейсами элементов управления
Теперь, когда вы увидели пользовательские элементы управления в действии, давайте разберёмся, как они интегрируются с Signal Forms.
Интерфейсы элементов управления
Созданные вами компоненты BasicInput и BasicToggle реализуют определённые интерфейсы элементов управления, которые указывают Signal Forms, как с ними взаимодействовать.
FormValueControl
FormValueControl — это интерфейс для большинства типов полей ввода: текстовых и числовых полей, средств выбора даты, раскрывающихся списков и любых элементов управления, изменяющих одно значение. Когда компонент реализует этот интерфейс:
-
Обязательное свойство: компонент должен предоставлять сигнал модели
value -
Действие директивы FormField: привязывает значение поля формы к сигналу
valueвашего элемента управления
ВАЖНО: Элементы управления, реализующие FormValueControl, НЕ должны иметь свойство checked
FormCheckboxControl
FormCheckboxControl — это интерфейс для элементов управления в виде флажков: переключателей и любых элементов управления, представляющих логическое состояние «включено/выключено». Когда компонент реализует этот интерфейс:
-
Обязательное свойство: компонент должен предоставлять сигнал модели
checked -
Действие директивы FormField: привязывает значение поля формы к сигналу
checkedвашего элемента управления
ВАЖНО: Элементы управления, реализующие FormCheckboxControl, НЕ должны иметь свойство value
Необязательные свойства состояния
Оба интерфейса — FormValueControl и FormCheckboxControl — расширяют FormUiControl, базовый интерфейс с необязательными свойствами для интеграции с состоянием формы.
Все свойства необязательны. Реализуйте только те, которые нужны вашему элементу управления.
Состояние взаимодействия
Отслеживайте взаимодействие пользователей с элементом управления:
| Свойство | Назначение |
|---|---|
touched | Взаимодействовал ли пользователь с полем |
dirty | Отличается ли значение от исходного |
Состояние проверки
Отображайте пользователям сообщения о результатах проверки:
| Свойство | Назначение |
|---|---|
errors | Массив текущих ошибок проверки |
valid | Является ли поле допустимым |
invalid | Есть ли у поля ошибки проверки |
pending | Выполняется ли асинхронная проверка |
Состояние доступности
Управляйте тем, могут ли пользователи взаимодействовать с полем:
| Свойство | Назначение |
|---|---|
disabled | Отключено ли поле |
disabledReasons | Причины отключения поля |
readonly | Доступно ли поле только для чтения (видно, но недоступно для редактирования) |
hidden | Скрыто ли поле |
ПРИМЕЧАНИЕ: disabledReasons — это массив объектов DisabledReason. Каждый объект имеет свойство field (ссылка на дерево полей) и необязательное свойство message. Получить сообщение можно через reason.message.
Ограничения проверки
Получайте значения ограничений проверки из формы:
| Свойство | Назначение |
|---|---|
required | Обязательно ли поле |
min | Минимальное числовое значение (undefined, если ограничение не задано) |
max | Максимальное числовое значение (undefined, если ограничение не задано) |
minLength | Минимальная длина строки (undefined, если ограничение не задано) |
maxLength | Максимальная длина строки (undefined, если ограничение не задано) |
pattern | Массив шаблонов регулярных выражений для проверки |
Метаданные поля
| Свойство | Назначение |
|---|---|
name | Атрибут name поля (уникальный для всех форм и приложений) |
В разделе «Добавление сигналов состояния» ниже показано, как реализовать эти свойства в элементах управления.
Как работает директива FormField
Директива [formField] определяет, какой интерфейс реализует ваш элемент управления, и автоматически связывает соответствующие сигналы:
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, FormField, required} from '@angular/forms/signals';
import {CustomInput} from './custom-input';
import {CustomToggle} from './custom-toggle';
@Component({
selector: 'app-my-form',
imports: [FormField, CustomInput, CustomToggle],
template: `
<form novalidate>
<app-custom-input [formField]="userForm.username" />
<app-custom-toggle [formField]="userForm.subscribe" />
</form>
`,
})
export class MyForm {
formModel = signal({
username: '',
subscribe: false,
});
userForm = form(this.formModel, (schemaPath) => {
required(schemaPath.username, {message: 'Username is required'});
});
}
СОВЕТ: Полное описание создания моделей формы и управления ими см. в руководстве «Модели формы».
При привязке [formField]="userForm.username" директива FormField выполняет следующие действия:
- Определяет, что элемент управления реализует
FormValueControl - Обращается к
userForm.username().value()внутри и связывает его с сигналом моделиvalueвашего элемента управления - Привязывает сигналы состояния формы (
disabled(),errors()и т. д.) к необязательным входным сигналам вашего элемента управления - Обновления выполняются автоматически благодаря реактивности сигналов
Добавление сигналов состояния
Показанные выше минимальные элементы управления работают, но не реагируют на состояние формы. Вы можете добавить необязательные входные сигналы, чтобы элементы управления реагировали на отключённое состояние, отображали ошибки проверки и отслеживали взаимодействие пользователя.
Вот подробный пример реализации распространённых свойств состояния:
import {Component, model, input, ChangeDetectionStrategy} from '@angular/core';
import {
FormValueControl,
WithOptionalFieldTree,
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.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>
}
`,
})
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 WithOptionalFieldTree<ValidationError>[]>([]);
}
Теперь этот элемент управления можно использовать с проверкой и управлением состоянием:
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, FormField, required, email} from '@angular/forms/signals';
import {StatefulInput} from './stateful-input';
@Component({
imports: [FormField, StatefulInput],
template: `
<form novalidate>
<label>
Email
<app-stateful-input [formField]="loginForm.email" />
</label>
</form>
`,
})
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'});
});
}
Когда пользователь вводит недопустимый адрес электронной почты, директива FormField автоматически обновляет invalid() и errors(). Ваш элемент управления может отображать сообщения о результатах проверки.
Типы сигналов для свойств состояния
Для большинства свойств состояния используется input() (доступный форме только для чтения). Используйте model() для touched, если элемент управления обновляет его при взаимодействии с пользователем. Свойство touched — единственное, которое может использовать model(), input() или OutputRef, в зависимости от ваших потребностей.
Преобразование значения
Иногда элементы управления отображают значения иначе, чем они хранятся в модели формы: например, средство выбора даты может показывать «15 января 2024 г.», сохраняя «2024-01-15», а поле ввода денежной суммы — отображать «$1,234.56», сохраняя 1234.56.
Используйте linkedSignal() (из @angular/core), чтобы преобразовать значение модели для отображения, а для разбора пользовательского ввода в исходный формат хранения обрабатывайте события ввода:
import {formatCurrency} from '@angular/common';
import {ChangeDetectionStrategy, Component, linkedSignal, model} from '@angular/core';
import {FormValueControl} from '@angular/forms/signals';
@Component({
selector: 'app-currency-input',
template: `
<input
type="text"
[value]="displayValue()"
(input)="displayValue.set($event.target.value)"
(blur)="updateModel()"
/>
`,
})
export class CurrencyInput implements FormValueControl<number> {
// Stores numeric value (1234.56)
readonly value = model.required<number>();
// Stores display value ("1,234.56")
readonly displayValue = linkedSignal(() => formatCurrency(this.value(), 'en', 'USD'));
// Update the model from the display value.
updateModel() {
this.value.set(parseCurrency(this.displayValue()));
}
}
// Converts a currency string to a number (e.g. "USD1,234.56" -> 1234.56).
function parseCurrency(value: string): number {
return parseFloat(value.replace(/^[^\d-]+/, '').replace(/,/g, ''));
} Интеграция с проверкой
Элементы управления отображают состояние проверки, но сами проверку не выполняют. Проверка происходит в схеме формы: элемент управления получает от директивы FormField сигналы invalid() и errors() и отображает их (как показано выше на примере StatefulInput).
Директива FormField также передаёт значения ограничений проверки, такие как required, min, max, minLength, maxLength и pattern. Элемент управления может использовать их для улучшения интерфейса:
export class NumberInput implements FormValueControl<number> {
value = model<number>(0);
// Constraint values from schema validation rules
required = input<boolean>(false);
min = input<number | undefined>(undefined);
max = input<number | undefined>(undefined);
}
Добавив в схему правила проверки min() и max(), вы передадите эти значения элементу управления через директиву FormField. Используйте их для установки атрибутов HTML5 или отображения подсказок об ограничениях в шаблоне.
ВАЖНО: Не реализуйте логику проверки в элементе управления. Определите правила проверки в схеме формы и позвольте элементу управления отображать результаты:
// Avoid: Validation in control
export class BadControl implements FormValueControl<string> {
value = model<string>('');
isValid() {
return this.value().length >= 8;
} // Don't do this!
}
// Good: Validation in schema, control displays results
accountForm = form(this.accountModel, (schemaPath) => {
minLength(schemaPath.password, 8, {message: 'Password must be at least 8 characters'});
}); Следующие шаги
В этом руководстве показано, как создавать пользовательские элементы управления, интегрированные с Signal Forms. В связанных руководствах рассматриваются другие аспекты 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/custom-controls