📱 Подписаться
IT и цифровая трансформация

[Перевод] Signal Forms в Angular

📰 Habr 👁️ 4 просмотров

kmoseenk2 часа назад

Signal Forms в Angular

Время на прочтение7 минОхват и читатели2.7KБлог компании OTUSJavaScript*Angular*ТуториалПереводАвтор оригинала: ninja-squadВ Angular v21 у разработчиков появится новый, пока что экспериментальный, способ создавать формы: Signal Forms.

После многих лет работы с формами, управляемыми шаблоном, template-driven forms (ngModel), и реактивными формами, reactive forms (formGroup/formControl), у нас появился третий подход, целиком основанный на сигналах и доступный в пакете @angular/forms/signals.

Это первая часть нашей серии о Signal Forms в Angular. В этой статье мы разберем основы: создание форм, обработку отправки и добавление валидации.

Создание формы с помощью form()

Поскольку этот новый подход основан на сигналах, первое, что нужно сделать в компоненте, — определить сигнал, который будет хранить значение формы. Затем с помощью функции form()из пакета @angular/forms/signals можно создать FieldTree, чтобы изменять значение этого сигнала.

Здесь я хочу написать компонент Login, в котором пользователь вводит свои учетные данные:

// 👇 сигнал для хранения учетных данных формы
private readonly credentials = signal({
login: '',
password: ''
});
// 👇 форма, созданная из сигнала credentials (тип — `FieldTree`)
protected readonly loginForm = form(this.credentials);Дальше нужно привязать каждый input/select/textarea к полю формы. Для этого используется единственная директива — Field:

<form (submit)="authenticate($event)">
<label for="login">Логин</label>
<input id="login" [field]="loginForm.login" />
<label for="password">Пароль</label>
<input id="password" [field]="loginForm.password" />
<button type="submit">Войти</button>
</form>Вот и всё. Одной директивы [field] достаточно, чтобы ваши поля ввода автоматически связались с полями формы. Изменение в поле ввода обновляет соответствующее свойство сигнала, а изменение сигнала, в свою очередь, обновляет значение в поле ввода.

Отправка формы с помощью submit()

Чтобы отправить форму, можно подписаться на стандартное событие submit у элемента form и вызвать метод вашего компонента.

Внутри этого метода можно использовать функцию submit() из пакета @angular/forms/signals для обработки отправки формы. Эта функция:

• помечает все поля как затронутые,
• проверяет, валидна ли форма,
• и, если форма валидна, вызывает переданное вами асинхронное действие, передавая в него форму в качестве аргумента.Аргументом будет тот же объект — FieldTree, который возвращает функция form(), то есть тот же самый объект, что и loginForm.

Объект FieldTree — одно из ключевых понятий в signal forms. Ваш loginForm — это FieldTree. То же самое относится и к loginForm.login, и к loginForm.password.

FieldTree, как и Signal, — это объект, который одновременно является функцией. Если вызвать его как функцию, например loginForm(), он вернет объект FieldState с несколькими сигнальными свойствами, которые описывают состояние поля:

• value(): текущее значение поля (может обновляться с задержкой)
• controlValue(): текущее значение в элементе управления формы, элементе формы, всегда актуальное
• touched(): было ли поле затронуто пользователем
• dirty(): перестало ли поле быть исходным
• disabled(): отключено ли поле
• disabledReasons(): если поле отключено, по каким причинам
• hidden(): скрыто ли поле
• readonly(): доступно ли поле только для чтения
• submitting(): находится ли поле в процессе отправкиНекоторые свойства связаны с валидацией — об этом мы поговорим ниже:

• pending(): находится ли поле в ожидании, то есть выполняются ли асинхронные валидаторы
• valid(): валидно ли поле, то есть прошли ли все валидаторы. Обратите внимание: valid() возвращает false, пока не завершатся все асинхронные валидаторы
• invalid(): невалидно ли поле
• errors(): ошибки поля, если они есть
• errorSummary(): ошибки поля и всех его вложенных полей, если они естьУ объекта также есть несколько методов для изменения состояния поля:

• reset() — помечает поле как pristine и untouched, но не сбрасывает его значение
• markAsTouched()/markAsDirty() — помечают поле как затронутое или измененноеМожно также получить FieldState для вложенного поля, например через loginForm.login(). Тогда каждое свойство будет описывать состояние уже этого конкретного вложенного поля: его значение, признак изменения и так далее.

Итак, возвращаясь к отправке формы, можно написать так:

protected async authenticate(event: SubmitEvent) {
// 👇 предотвращает стандартное поведение браузера
event.preventDefault();
// 👇 отправляет форму, если она валидна, вызывая метод authenticate (Promise)
await submit(this.loginForm, form => {
const { login, password } = form().value();
return this.userService.authenticate(login, password);
});
}Обратите внимание: функция, которую вы передаете в submit(), должна возвращать Promise. Поэтому, если ваш сервис возвращает Observable, его придется преобразовать в Promise, например с помощью функции firstValueFrom() из rxjs.

Теперь давайте в нашу форму добавим валидацию.

Добавление валидации с помощью встроенных валидаторов

Angular предоставляет систему валидации, основанную на функциях, с помощью которых можно программно задавать ограничения для значения поля.

Как и в предыдущих системах работы с формами, здесь есть два типа валидаторов:

• синхронные валидаторы, которые выполняются сразу,
• асинхронные валидаторы, которые возвращают Promise и запускаются только в том случае, если поле успешно прошло все синхронные валидаторы.Сам фреймворк предоставляет набор встроенных синхронных валидаторов — тех же самых, что используются в реактивных формах и формах, управляемых шаблоном:

• required(field) — помечает поле как обязательное
• minLength(field, length) — задает минимальную длину для строк и массивов
• maxLength(field, length) — задает максимальную длину для строк и массивов
• min(field, min) — задает минимальное значение для чисел
• max(field, max) — задает максимальное значение для чисел
• email(field) — проверяет корректность адреса электронной почты
• pattern(field, pattern) — проверяет значение на соответствие регулярному выражению Эти функции принимают поле, которое нужно проверить, в качестве первого аргумента, а дальше — дополнительные аргументы в зависимости от конкретного валидатора.

Давайте создадим еще одну форму — Register, которая позволит пользователю создать учетную запись:

protected readonly accountForm = form(
signal({
email: '',
password: ''
}),
// 👇 добавляем валидаторы к полям вместе с сообщениями об ошибках
form => {
// email обязателен
required(form.email, { message: 'Необходимо указать email' });
// должен быть корректный email
email(form.email, { message: 'Некорректный email' });
// пароль обязателен
required(form.password, { message: 'Необходимо указать пароль' });
// должен содержать не менее 6 символов
minLength(form.password, 6, {
// сообщением может быть и функция, если нужен доступ к текущему значению или состоянию поля
message: password => `Пароль должен содержать не менее 6 символов, а сейчас в нем только ${password.value().length}`
});
}
);Теперь каждый валидатор будет срабатывать при изменении значения поля. Если проверка не пройдена, в свойство errors() этого поля добавляется объект ValidationError. ValidationError — это объект с несколькими свойствами:

• kind: тип ошибки, например required, minLength и так далее
• field: поле, вызвавшее ошибку
• message: необязательное человекочитаемое сообщение. По умолчанию сообщения нет, поэтому, если оно вам нужно, его необходимо передать самостоятельно при подключении валидатораНекоторые валидаторы могут добавлять в объект ошибки и дополнительные свойства. Например, валидатор minLength добавляет свойство minLength, поэтому его можно использовать при выводе текста ошибки.

Эти ошибки можно показывать в шаблоне, перебирая содержимое свойства errors() у поля:

<input id="email" [field]="accountForm.email" />
@let email = accountForm.email();
@if (email.touched() && !email.valid()) {
<div id="email-errors">
@for (error of email.errors(); track error.kind) {
<div>{{ error.message }}</div>
}
</div>
}Встроенные валидаторы теперь умеют больше, чем раньше: они также добавляют к полю метаданные. Это может быть полезно, чтобы понять, применен ли к полю тот или иной валидатор. Например, в шаблоне можно добавить звездочку рядом с подписью обязательных полей:

<label for="email">
<span>Email</span>
@let isEmailRequired = accountForm.email().metadata(REQUIRED);
@if (isEmailRequired()) {
<span>*</span>
}
</label>Метод metadata(REQUIRED) возвращает сигнал, значение которого может быть true или false. Но некоторые валидаторы сохраняют в этих метаданных и дополнительные сведения. Например, сигнал, возвращаемый metadata(MIN_LENGTH), содержит минимальную длину.

Валидация по стандартной схеме с помощью validateStandardSchema()

Помимо встроенных валидаторов, о которых шла речь выше, signal forms предлагают новый способ задавать ограничения для поля — через валидацию по схеме (schema).

В последние годы этот подход стал популярным благодаря таким библиотекам, как Zod и Valibot.

Обе позволяют описать схему, которая представляет данные: с помощью функций задается тип каждого поля, а также ограничения для этих полей. Более того, эти библиотеки и ряд других объединили усилия, чтобы определить общий стандарт — Standard Schema, который предоставляет единый интерфейс под названием StandardSchemaV1 для использования в разных библиотеках.

Angular опирается на этот стандарт и предоставляет функцию validateStandardSchema(), которая принимает такую схему StandardSchemaV1.

Эту схему можно описать с помощью любой библиотеки, которая вам больше нравится. Давайте воспользуемся Zod Mini, чтобы задать схему валидации для формы Register:

(form) => {
validateStandardSchema(
form,
z.object({ email: z.string().check(z.email()), password: z.string().check(z.minLength(6)) })
);
};Здесь мы не задаем сообщения об ошибках вручную. Ошибки будут автоматически сформированы функцией validateStandardSchema(), которая возвращает ошибки типа StandardSchemaValidationError. У этих ошибок те же свойства, что и у рассмотренного ранее ValidationError.

Например, если ввести слишком короткий пароль, вы получите примерно такую ошибку:

Слишком короткое значение: ожидается строка длиной не менее 6 символовЭто тоже можно настроить. Zod Mini позволяет задавать сообщения об ошибках прямо при описании схемы, например с помощью такого выражения: z.string().check(z.minLength(6, { message: issue => Пароль должен содержать не менее ${issue.minimum} символов })).

Что дальше?

Мы разобрали основы Angular Signal Forms: создание форм, обработку отправки и добавление базовой валидации с помощью встроенных валидаторов или валидации по схеме через библиотеки вроде Zod.

В следующей части мы перейдем к более продвинутым темам: созданию пользовательских валидаторов, межполевой валидации и асинхронной валидации.

В Angular чаще всего спотыкаются на архитектуре, типизации и управлении состоянием. На курсе «Angular-разработчик» это разбирают на практике: как строить модули, работать с RxJS и писать поддерживаемый код без хаоса в компонентах. Чтобы узнать, подойдет ли вам программа курса,пройдите вступительный тест.

Для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:

• 26 марта в 20:00. «Angular + ИИ: как писать код быстрее с помощью LLM». Записаться
• 9 апреля в 20:00. «Angular без RxJS? Пишем реактивные формы на сигналах». Записаться
• 21 апреля в 20:00. «Архитектура Angular-приложения: как писать масштабируемый frontend». ЗаписатьсяТеги:• js
• Angular
• Signal Forms
• сигналы
• реактивные формы
• валидация форм
• FieldTree
• TypeScript
• RxJS
• frontend разработкаХабы:• Блог компании OTUS
• JavaScript
• Angular

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал