Signal Forms в Angular
Цього місяця в Angular-спільноті відбулася справді цікава подія: в основну гілку фреймворку нарешті влили експериментальну фічу Signal Forms.
А чому це така цікава новина? — спитаєте ви. Адже ми ж роками працювали з реактивними формами й якось жили з цим.
Але от у чому річ: Signal Forms нарешті закривають цілу купу болючих моментів:
- щоб створити навіть просту форму, треба багато boilerplate-коду
FormGroupтаFormControl; - ручні підписки на
valueChanges, щоб UI залишався синхронним; - нескінченні
if/elseдля виводу помилок; - дублювання логіки валідаторів у різних полях.
У цій статті я покажу, чому форми на сигналах — це справді новий підхід.
Базова форма
В основі нового Signal Forms API лежить функція form().
Замість того, щоб вручну збирати FormGroup і FormControl, тепер достатньо передати їй сигнал-модель (тобто ваш стан форми) — і Angular сам побудує для вас форму, яка працює на сигналах.
Візьмемо простий приклад — форму реєстраці на конференцію:
protected readonly registration = signal<ConferenceRegistration>({
fullName: '',
email: '',
arrivalDate: new Date(),
needAccommodation: false,
accommodationNotes: ''
});
interface ConferenceRegistration {
fullName: string;
email: string;
arrivalDate: Date;
needAccommodation: boolean;
accommodationNotes: string;
}
Далі все максимально просто — передаємо модель у form():
protected readonly registrationForm = form(this.registration);
І на цьому все — форма створена. Жодних «танців» із FormGroup та зайвим boilerplate-кодом.
Прив’язка інпутів через директиву
У шаблоні нам більше не потрібні formControlName чи ngModel. Тепер достатньо використати нову директиву [control], яка напряму з’єднує поле форми з HTML-елементом:
<h2>Conference Registration Form</h2>
<form (submit)="register(); $event.preventDefault()">
<input [control]="registrationForm.fullName" placeholder="Ваше ім’я" type="text" />
<input [control]="registrationForm.email" placeholder="Email" type="email" />
<input [control]="registrationForm.arrivalDate" type="date" />
<label>
<span>Потрібне проживання?</span>
<input [control]="registrationForm.needAccommodation" type="checkbox" />
</label>
@if (registration().needAccommodation) {
<textarea [control]="registrationForm.accommodationNotes" placeholder="Деталі проживання"></textarea>
}
<button type="submit">Зареєструватися</button>
</form>
Директива [control] робить усе набагато прозорішим:
- не потрібно писати зайві назви чи ідентифікатори для кожного поля;
- не потрібно вручну зв’язувати форму з інпутами;
- кожна властивість моделі автоматично «живе» у своєму інпуті.
Валідація
У Signal Forms правила перевірки можна задати прямо в form() — це робить код набагато чистішим.
Наприклад, додамо валідацію для полів fullName та email:
protected readonly registrationForm = form(this.registration, (path) => {
required(path.fullName),
minLength(path.fullName, 3),
maxLength(path.fullName, 100),
required(path.email),
email(path.email)
});
У шаблоні можна вивести повідомлення про помилки для кожного поля:
<input [control]="registrationForm.fullName" placeholder="Ваше ім'я" type="text" />
@for(error of registrationForm.fullName().errors(); track error) {
@if(error.kind === 'required') {
<div class="validation-error">Це поле є обов'язковим</div>
} @else if(error.kind === 'minLength') {
<div class="validation-error">Мінімальна довжина — 3 символи</div>
} @else if(error.kind === 'maxLength') {
<div class="validation-error">Максимальна довжина — 100 символів</div>
}
}
<input [control]="registrationForm.email" placeholder="Email" type="email" />
@for(error of registrationForm.email().errors(); track error) {
@if(error.kind === 'required') {
<div class="validation-error">Введіть email</div>
} @else if(error.kind === 'email') {
<div class="validation-error">Неправильний формат email</div>
}
}
<button type="submit" [disabled]="!registrationForm().valid()">Зареєструватися</button>
Це працює, але є одна проблема: такий підхід виходить занадто оверхедним рішенням. З кожним новим валідатором кількість коду росте, а шаблон перетворюється в ад.
Менше if/else для помилок
Можна зробити набагато краще, повідомлення прив’язати безпосередньо до валідаторів. Тобто нам вже не потрібні купа if/else для виводу помилок — достатньо просто у циклі пробіжатись по всім повідомлення цього інпуту.
protected readonly registrationForm = form(this.registration, (path) => {
required(path.fullName, { message: "Вкажіть ваше ім'я" }),
minLength(path.fullName, 3, { message: "Мінімум 3 символи" }),
maxLength(path.fullName, 100, { message: "Максимум 100 символів" }),
required(path.email, { message: "Введіть email" }),
email(path.email, { message: "Невірний формат email" })
});
Тепер шаблон виглядає набагато чистішим:
<input [control]="registrationForm.fullName" placeholder="Ваше ім’я" type="text" />
@for(error of registrationForm.fullName().errors(); track error) {
<div class="validation-error">{{ error.message }}</div>
}
<input [control]="registrationForm.email" placeholder="Email" type="email" />
@for(error of registrationForm.email().errors(); track error) {
<div class="validation-error">{{ error.message }}</div>
}
Приємний бонус: повідомлення не обов’язково мають бути «жорстко зашитим» текстом. Замість цього ви можете прокидати ключ перекладу, а ваша i18n-система (наприклад, Transloco) автоматично підтягне відповідний текст потрібною мовою.
Одна схема — кілька полів
У нас два поля — fullNameта email — можуть мати однакові правила валідації. Щоб не дублювати код, можна винести їх у схему і застосовувати повторно:
import { Schema, schema, apply, required, minLength, maxLength, email } from '@angular/forms/signals';
// Схема для текстових полів
const textSchema: Schema<string> = schema((fieldPath) => {
required(fieldPath, { message: 'Це поле є обов'язковим' });
minLength(fieldPath, 3, { message: 'Мінімум 3 символи' });
maxLength(fieldPath, 100, { message: 'Максимум 100 символів' });
});
// Створюємо форму і застосовуємо схему до кількох полів
protected readonly registrationForm = form(this.registration, (fieldPath) => {
apply(fieldPath.fullName, textSchema);
apply(fieldPath.email, schema((f) => {
// email поле має додаткову валідацію для формату
apply(f, textSchema); // наслідуємо базову схему тексту
email(f, { message: 'Невірний формат email' }); // додатковий валідатор
}));
});
Тепер валідація стала ще більш DRY.
Умовна валідація
Іноді правила валідації залежать від інших полів форми. Наприклад, у формі реєстрації учасника конференції поле accommodationNotes має бути обов’язковим тільки якщо учасник потребує проживання.
Раніше цеб означало підписку на valueChanges, ручне оновлення валідаторів і виклик updateValueAndValidity().
З Signal Forms все значно простіше: валідатори можуть отримувати умову when, яка визначає, коли вони повинні застосовуватися.
protected readonly registrationForm = form(this.registration, (fieldPath) => {
// Базова валідація для імені та email
...
// Валідація для деталей проживання
required(fieldPath.accommodationNotes, {
when: ({ valueOf }) => valueOf(fieldPath.needAccommodation) === true,
message: 'Будь ласка, вкажіть деталі проживання'
});
});
Шаблон форми буде виглядати наступним чином:
<label>
<span>Потрібне проживання?</span>
<input [control]="registrationForm.needAccommodation" type="checkbox" />
</label>
@if (registration().needAccommodation) {
<textarea [control]="registrationForm.accommodationNotes" placeholder="Деталі проживання"></textarea>
@for(error of registrationForm.accommodationNotes().errors(); track error) {
<div class="validation-error">{{ error.message }}</div>
}
}
Тепер форма вимагає введення деталей проживання тільки коли це дійсно потрібно!
Обробка сабміта
Форми не лише перевіряють дані, а й відправляють їх на бекенд. У Signal Forms для цього є зручні хелпери, давайте поглянемо на них.
submit() дозволяє:
- Відслідковувати стан відправки (
submitting) - Обробляти серверні помилки через систему помилок форми
- Скидати форму після успішної відправки
protected register() {
submit(this.registrationForm, async (form) => {
try {
await firstValueFrom(
this.httpClient.post(
'https://api.conference.com/registrations',
JSON.stringify(this.registration())
)
);
form().reset(); // очищаємо форму після успішної відправки
return undefined;
} catch (e) {
// перетворюємо серверну помилку на помилку форми
return [
{
kind: 'server',
message: (e as Error).message,
},
];
}
});
}
У шаблоні:
<button [disabled]="!registrationForm().valid() || registrationForm().submitting()" type="button" (click)="register()" > Зареєструватися </button>
Тепер кнопка активна лише коли форма валідна та ще не відправляється. Серверні помилки відображаються як частина форми, і після успішної відправки всі поля очищаються.
Шо по ітогу?
Signal Forms справді відчуваються як щось нове для Angular. Раніше робота з формами означала нескінченні FormGroup, підписки на valueChanges, дублювання логіки валідаторів і купа if/else для помилок. З сигналами усе це значно спрощується.
Нагадаю, це лише експериментальна версія в Angular v21.next і API продовжує розвиватися!
Буду вдячний за лайки, якщо вам сподобалась стаття 😊
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів