javascript

Signal Forms в Angular

  • суббота, 21 марта 2026 г. в 00:00:46
https://habr.com/ru/companies/otus/articles/1012734/

В 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 для обработки отправки формы. Эта функция:

  1. помечает все поля как затронутые,

  2. проверяет, валидна ли форма,

  3. и, если форма валидна, вызывает переданное вами асинхронное действие, передавая в него форму в качестве аргумента.

Аргументом будет тот же объект — 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». Записаться