23 сент. 2025

Сигнальные формы: самая ожидаемая фича уже здесь

Несколько дней назад в мире Angular произошло волнующее событие: экспериментальная ветка Signal Forms (сигнальных форм) была влита в мастер-веткуAngular. Это открыло доступ к одному из самых ожидаемых обновлений.

Почему это так важно? Потому что сигнальные формы, наконец, решают проблемы, которые раздражали годами:

  • мучительную настройку FormGroup и FormControl

  • прописываемые руками подписки на valueChanges для синхронизации с UI

  • повторяющиеся блоки if/else для сообщений об ошибках

  • дублирование валидаторов для нескольких разных полей

В этой статье я на реальном примере — форме подачи заявки на конференцию — покажу шаг за шагом, как сигнальные формы упрощают процесс разработки.

Шаг 1: Базовая форма

В основе нового Signal Forms API лежит функция form().

Вместо ручного создания экземпляров FormGroup и FormControl, вы просто передаёте ей сигнальную модель (состояющую из стейта формы), и Angular создаёт для вас форму на основе сигналов. В нашем случае модель — это объект TalkProposal.

protected readonly talkProposal = signal<TalkProposal>({
  title: '',
  speaker: '',
  preferredDate: new Date(),
  requestTravelSponsorship: false,
  travelJustification: ''
});
interface TalkProposal {
  title: string;
  speaker: string;
  preferredDate: Date;
  requestTravelSponsorship: boolean;
  travelJustification: string;
}

Затем мы передаём это в функцию form():

protected readonly talkProposalForm = form(this.talkProposal);

Вот и всё — форма создана.

Привязка инпутов с помощью директивы control

В шаблоне больше не нужны директивы formControlName или ngModel. Вместо этого мы используем новую директиву [control] для подключения контролов к инпутам.

<h2>Conference Talk Proposal Form</h2>
<form (submit)="saveProposal(); $event.preventDefault()">
<input [control]="talkProposalForm.title" placeholder="Enter Talk Title" type="text" />
<input [control]="talkProposalForm.speaker" placeholder="Enter Speaker Name" type="text" />
  <input [control]="talkProposalForm.preferredDate" type="date" />

  <label>
    <span>Request travel sponsorship</span>
    <input [control]="talkProposalForm.requestTravelSponsorship" type="checkbox" />
  </label>
  @if (talkProposal().requestTravelSponsorship) {
    <textarea [control]="talkProposalForm.travelJustification" placeholder="Enter Travel Justification"></textarea>
  }
  <button type="submit">Submit Talk Proposal</button>
</form>	

Директива [control] делает привязку простой и декларативной — нет необходимости искать элементы управления по имени или подключать их вручную. Каждое поле в модели само сопоставляется с инпутами.

Шаг 2: Валидация

Форма без валидации теряет смысл. В сигнальных формах правила валидации объявляются прямо внутри функции form(). Пропишем некоторые из них:

protected readonly talkProposalForm = form(this.talkProposal, (path) => {
  required(path.title),
  minLength(path.title, 3),
  maxLength(path.title, 50),
  required(path.speaker),
  minLength(path.speaker, 3),
  maxLength(path.speaker, 50)
});

Чтобы отобразить правильное сообщение об ошибке, мы проверяем её тип:

<input [control]="talkProposalForm.title" placeholder="Enter Talk Title" type="text" />
@for(error of talkProposalForm.title().errors(); track error){
  @if(error.kind === 'required'){
    <div class="validation-error">This field is required</div>
  } @else if(error.kind === 'minLength'){
    <div class="validation-error">Min length is 3</div>
  } @else if(error.kind === 'maxLength'){
    <div class="validation-error">Max length is 50</div>
  }
}
<input [control]="talkProposalForm.speaker" placeholder="Enter Speaker Name" type="text" />
@for(error of talkProposalForm.speaker().errors(); track error){
  @if(error.kind === 'required'){
    <div class="validation-error">This field is required</div>
  } @else if(error.kind === 'minLength'){
    <div class="validation-error">Min length is 3</div>
  } @else if(error.kind === 'maxLength'){
    <div class="validation-error">Max length is 50</div>
  }
}
<button type="submit" [disabled]="!talkProposalForm().valid()">Submit Talk Proposal</button>

Это работает, но код быстро разрастается — особенно с добавлением новых валидаторов

Шаг 2.1: Определение сообщений в валидаторах

Сигнальные формы исправляют ситуацию, позволяя прикреплять сообщения напрямую к валидаторам. Таким образом, нам больше не нужен блок if/else — мы можем просто отображать ошибку.

protected readonly talkProposalForm = form(this.talkProposal, (path) => {
  required(path.title, { message: 'This field is required' }),
  minLength(path.title, 3, { message: 'Enter minimum 3 characters' }),
  maxLength(path.title, 50, { message: 'Enter max 50 characters' }),
  required(path.speaker, { message: 'This field is required' }),
  minLength(path.speaker, 3, { message: 'Enter minimum 3 characters' }),
  maxLength(path.speaker, 50, { message: 'Enter max 50 characters' })
});	

Сам шаблон предельно упрощается:

<input [control]="talkProposalForm.title" placeholder="Enter Talk Title" type="text" />
@for(error of talkProposalForm.title().errors(); track error){
  <div class="validation-error"></div>
}
<input [control]="talkProposalForm.speaker" placeholder="Enter Speaker Name" type="text" />
@for(error of talkProposalForm.speaker().errors(); track error){
  <div class="validation-error"></div>
}

Если сообщение должно быть не просто обычным текстом, вы, конечно, можете передать ключ перевода и обработать его с помощью i18n, например, transloco.

Шаг 2.2: Повторное использование валидаторов через cхемы (Schemas)

Обратите внимание, что title и speaker имеют одинаковые правила. Вместо их дублирования мы можем определить схему один раз и применить её к нескольким полям:

import { Schema, schema, apply } from '@angular/forms/signals';
const textSchema: Schema<string> = schema((fieldPath) => {
  required(fieldPath, { message: 'This field is required' }),
  minLength(fieldPath, 3, { message: 'Enter minimum 3 characters' }),
  maxLength(fieldPath, 50, { message: 'Enter max 50 characters' })
});
protected readonly talkProposalForm = form(this.talkProposal, (fieldPath) => {
  apply(fieldPath.title, textSchema),
  apply(fieldPath.speaker, textSchema)
});

Код стал чище и теперь соответствует принципу DRY.

Шаг 3: Условная валидация

Иногда правила валидаторов не статичны — они зависят от других полей в форме. Например, в нашей форме заявки поле обоснования поездки должно быть обязательным, только если пользователь запрашивает компенсацию перелёта.

В классических формах Angular это обычно означало написание дополнительной логики: подписку на valueChanges, ручное обновление валидаторов и вызов updateValueAndValidity().

Сигнальные формы упрощают процесс. Валидаторы теперь могут принимать условие when, которое определяет, должно ли примениться правило.

Вот как мы можем добавить условный валидатор для поля travelJustification:

protected readonly talkProposalForm = form(this.talkProposal, (fieldPath) => {
  apply(fieldPath.title, textSchema),
  apply(fieldPath.speaker, textSchema),
  required(fieldPath.travelJustification, {
    when: ({ valueOf }) => valueOf(fieldPath.requestTravelSponsorship) === true,
    message: 'Justification for travelling is required.'
  })
});

Шаблон сильно не меняется — мы просто добавляем отображение ошибки:

<label>
  <span>Request travel sponsorship</span>
  <input [control]="talkProposalForm.requestTravelSponsorship" type="checkbox" />
</label>
@if (talkProposal().requestTravelSponsorship) {
  <textarea
    [control]="talkProposalForm.travelJustification"
    placeholder="Enter Travel Justification"
  ></textarea>
  @for(error of talkProposalForm.travelJustification().errors(); track error){
    <div class="validation-error"></div>
  }
}

Теперь форма просит обосновать необходимость компенсации только тогда, когда это актуально.

Шаг 4: Обработка отправки данных формы

Формы не только проверяют ввод — обычно им нужно отправлять данные на бэк. В сигнальных формах и для этого есть встроенные помощники.

С помощью новой функции submit() вы можете:

  • отслеживать состояние отправки прямо внутри формы

  • обрабатывать ошибки сервера в рамках системы ошибок формы

  • сбрасывать поля после успешной отправки данных

Вот как мы можем подключить обработчик отправки к нашей форме:

protected saveProposal() {
  submit(this.talkProposalForm, async (form) => {
    try {
      await firstValueFrom(
        this.httpClient.post(
          'https://any-talk-proposal.ch/proposals',
          JSON.stringify(this.talkProposal())
        )
      );
      form().reset(); // clear the form on success
      return undefined;
    } catch (e) {
      // map server error into form errors
      return [
        {
          kind: 'server',
          message: (e as Error).message,
        },
      ];
    }
  });
}

Экземпляр формы отправляет сигнал .submitting(), который позволяет нам делать кнопку неактивной во время отправки формы.

Теперь кнопка отправки будет активна только тогда, когда форма валидна и в данный момент данные не отправляются.

В заключение

Сигнальные формы — глоток свежего воздуха для Angular-разработчиков. Годами работа с формами означала жонглирование жуткими настройками FormGroup, прописывание подписок на valueChanges, дублирование логики валидаторов и написание бесконечных блоков if/else для сообщений об ошибках. Как показал этот пример, с сигналами всё это становится гораздо проще.

И это всего лишь экспериментальная версия в Angular 21.next. API будет развиваться и улучшаться, но уже сейчас ясно, что сигнальные формы могут стать одним из главных улучшений в работе Angular-разработчиков.

Если вы устали от старого Angular Forms API — это именно то обновление, которого вы ждали.

Статья опубликована пользователем @schnabelelisa0 на Medium
https://medium.com/@schnabelelisa0/angular-signal-forms-the-most-awaited-feature-is-here-161fd722f573

Перевод Ани Зенковой (@a.zenkova)