В 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». Записаться
