
Несколько дней назад в мире 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)