Skip to main content

Різниця між формами, керованими шаблоном, та реактивними формами в Angular

Форми на основі шаблонів і реактивні форми (reactive forms) - два підходи, які Angular пропонує для роботи зі станом форм, валідацією та введенням даних. Відрізняються вони насамперед тим, де живе логіка: в HTML чи в TypeScript.

Теорія

TL;DR

  • Template-driven: підключаєш FormsModule, додаєш ngModel до інпутів, Angular сам будує модель форми з шаблону
  • Reactive: підключаєш ReactiveFormsModule, описуєш FormGroup у TypeScript, в шаблоні прив'язуєш через formControlName
  • Головна різниця: template-driven використовує двостороннє зв'язування через ngModel, реактивні форми - однонаправлений потік через observable valueChanges
  • До 5 статичних полів без динамічної логіки? Форми на основі шаблонів підійдуть. Динамічні поля, крос-польова валідація, помилки з сервера? Тільки реактивні.
  • Реактивні форми тестуються без DOM: створюєш FormGroup, викликаєш form.setValue(), перевіряєш form.valid

Швидкий приклад

Форма на основі шаблону:

html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)"> <input name="email" ngModel required email #email="ngModel"> <div *ngIf="email.invalid && email.touched">Невалідний email</div> <button [disabled]="userForm.invalid">Надіслати</button> </form>
typescript
onSubmit(form: NgForm) { console.log(form.value); // { email: "test@example.com" } }

Angular зчитує директиву ngModel і автоматично створює модель форми з шаблону. HTML тут у ролі джерела правди.

Реактивна форма:

typescript
form = new FormGroup({ email: new FormControl('', [Validators.required, Validators.email]) }); onSubmit() { console.log(this.form.value); // { email: "test@example.com" } }
html
<form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="email"> <div *ngIf="form.get('email')?.invalid && form.get('email')?.touched"> Невалідний email </div> <button [disabled]="form.invalid">Надіслати</button> </form>

TypeScript визначає модель першим. Шаблон лише прив'язується до неї.

Ключова різниця

Форми на основі шаблонів використовують директиви ngModel та NgForm, щоб автоматично побудувати й синхронізувати модель форми з HTML. FormControl явно не створюється - Angular робить це сам. Реактивні форми починаються з FormGroup, описаного в TypeScript. Це програмне дерево контролів, яке можна спостерігати через RxJS, змінювати напряму і тестувати без жодного DOM. Дані рухаються в один бік: від моделі до подання.

Коли що використовувати

  • Форма входу, контактна форма, 3-4 статичні поля: на основі шаблону, швидше написати
  • Поля, що з'являються або зникають залежно від дій користувача: реактивна (addControl() / removeControl())
  • Крос-польова валідація (пароль має збігатися з підтвердженням): реактивна, валідатор на рівні групи
  • Помилки з сервера, прив'язані до конкретного поля: реактивна (control.setErrors())
  • Потрібно тестувати логіку форми без браузера: реактивна, чистий TypeScript
  • Швидкий прототип або проста форма в Ionic: на основі шаблону

На практиці більшість команд переходить на реактивні форми після першого разу, коли потрібно динамічно додати поле до форми на основі шаблону і виявляється, що жодного API для цього немає.

Таблиця порівняння

ХарактеристикаTemplate-drivenReactive
НалаштуванняFormsModule + ngModel в HTMLReactiveFormsModule + FormGroup у TS
Потік данихДвостороннє зв'язування (ngModel)Однонаправлений (valueChanges observable)
ВалідаціяАтрибути HTML + помилки директивМасив Validators на FormControl
Динамічні поляОбхідні шляхи через *ngForaddControl() / removeControl()
Асинхронна валідаціяМожлива, більше налаштуваньВбудована через asyncValidators
ТестуванняПотрібен DOM, складніше ізолюватиТестуєш FormGroup напряму
Коли використовуватиПрості, статичні формиДинамічні, складні або тестовані форми

Як Angular обробляє обидва підходи

Для форм на основі шаблонів компілятор Angular сканує шаблон на директиви ngModel і будує NgForm, зіставляючи атрибути name з деревом директив. Якщо пропустити name на інпуті - це поле просто не потрапить у form.value.

Реактивні форми будують дерево FormGroup у TypeScript під час виконання. Зміни значень надходять через observable valueChanges і statusChanges без участі шаблону. Саме тому в тестах можна викликати form.setValue() і одразу читати form.valid - рендерити нічого не потрібно.

Типові помилки

1. Змішування ngModel з реактивними формами

html
<!-- Викликає помилку NG0100 або автоматичне перевизначення моделі --> <form [formGroup]="form"> <input formControlName="email" [(ngModel)]="model.email"> </form>

Обери один підхід. Якщо використовуєш formControlName, прибери ngModel.

2. Пряма зміна form.value

typescript
// Неправильно: Angular не бачить цієї зміни, UI не оновлюється this.form.value.email = 'new@email.com'; // Правильно this.form.patchValue({ email: 'new@email.com' });

form.value - це знімок стану, а не модель для запису.

3. Відсутній атрибут name у формах на основі шаблонів

html
<!-- Поле не потрапить у form.value --> <input ngModel required> <!-- Правильно --> <input name="email" ngModel required>

4. Забули підписатися на valueChanges

typescript
// Нічого не робить this.form.valueChanges; // Правильно this.form.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => console.log(value));

5. Очікуєш, що patchValue запустить асинхронний валідатор

typescript
// Асинхронний валідатор не спрацює, якщо встановлено updateOn: 'blur' this.emailControl.patchValue('new@example.com'); // Щоб примусово перевалідувати: this.emailControl.updateValueAndValidity();

Де зустрічається в реальних проектах

  • Wizards на Angular Material mat-stepper: реактивні, бо потрібна валідація між кроками
  • Інтеграція з NgRx: значення реактивного FormGroup зберігаються в стані для undo/redo
  • JSON-driven форми (наприклад, jsonforms.io): реактивні з FormArray для списків змінної довжини
  • Прості форми входу в Ionic: на основі шаблонів, менше коду

Питання на співбесіді

Q: Як додати нове поле у форму після того, як вона вже відрендерена?
A: Тільки в реактивній: this.form.addControl('phone', new FormControl('')). У формах на основі шаблонів API для цього немає. Можна обійти через *ngFor по масиву конфігів полів, але це швидко стає складним.

Q: Яка різниця між setValue і patchValue?
A: setValue вимагає передати всі поля FormGroup, інакше викине помилку. patchValue приймає частковий об'єкт і оновлює лише ті поля, які ти передав.

Q: Як тестувати реактивну форму без браузера?
A: Створюєш FormGroup напряму в тесті, викликаєш form.setValue() або form.patchValue(), потім перевіряєш form.valid або помилки конкретного контролу. Рендерити компонент не потрібно.

Q: Як уникнути запиту на сервер при кожному натисканні клавіші в асинхронному валідаторі?
A: Встанови updateOn: 'blur' в опціях FormControl. Тоді асинхронний валідатор запускається лише коли поле втрачає фокус, а не на кожну зміну.

Q: FormArray з 10 рядками. Користувач змінює їх порядок. Асинхронні валідатори спрацьовують не для тих рядків. Як виправити?
A: Відстежуй рядки за унікальним id, а не за індексом масиву. Додай debounceTime до асинхронного валідатора, щоб злити швидкі зміни в один виклик. Після зміни порядку виклич updateValueAndValidity() явно на потрібних контролах, не чекай автоматичного перезапуску. Звертатися до контролів через AbstractControl.at(i) після свопу безпечніше, ніж тримати застарілі посилання.

Приклади

Форма входу (на основі шаблону)

html
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)"> <input name="email" ngModel required email #emailField="ngModel"> <div *ngIf="emailField.invalid && emailField.touched">Введіть валідний email</div> <input name="password" type="password" ngModel required minlength="8"> <button [disabled]="loginForm.invalid">Увійти</button> </form>
typescript
onSubmit(form: NgForm) { if (form.valid) { console.log(form.value); // { email: "...", password: "..." } this.authService.login(form.value); } }

Angular будує NgForm з атрибутів name і директив ngModel. Правила валідації задаються атрибутами HTML. Просто і швидко - покриває більшість простих сценаріїв.

Реєстрація з крос-польовою валідацією (реактивна форма)

typescript
import { FormBuilder, FormGroup, Validators, AbstractControl } from '@angular/forms'; export class RegisterComponent { form: FormGroup; constructor(private fb: FormBuilder) { this.form = this.fb.group({ email: ['', [Validators.required, Validators.email]], confirmEmail: ['', Validators.required], password: ['', [Validators.required, Validators.minLength(8)]] }, { validators: this.emailMatchValidator }); } emailMatchValidator(group: AbstractControl) { const email = group.get('email')?.value; const confirm = group.get('confirmEmail')?.value; return email === confirm ? null : { mismatch: true }; } addPhoneField() { this.form.addControl('phone', this.fb.control('')); } }
html
<form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="email"> <input formControlName="confirmEmail" [class.error]="form.hasError('mismatch')"> <div *ngIf="form.hasError('mismatch')">Emails не збігаються</div> <button type="button" (click)="addPhoneField()">Додати телефон</button> <button [disabled]="form.invalid">Зареєструватись</button> </form>

Валідатор на рівні групи перевіряє весь FormGroup і повертає { mismatch: true }, коли emails різні. addPhoneField() додає новий контрол під час роботи без перезавантаження форми. Жоден з цих сценаріїв не реалізується у формах на основі шаблонів.

Асинхронний валідатор із перевіркою на сервері

typescript
import { AbstractControl } from '@angular/forms'; import { timer, of } from 'rxjs'; import { switchMap, map, catchError } from 'rxjs/operators'; asyncEmailValidator(control: AbstractControl) { return timer(500).pipe( switchMap(() => this.http.get<boolean>(`/api/check-email/${control.value}`) ), map(isTaken => isTaken ? { emailTaken: true } : null), catchError(() => of(null)) // мережева помилка - не блокуємо форму ); } // Використання в FormControl email = new FormControl('', { validators: [Validators.required, Validators.email], asyncValidators: [this.asyncEmailValidator.bind(this)], updateOn: 'blur' // перевіряємо тільки при втраті фокусу });

timer(500) дебаунсить виклик, щоб швидкий набір не генерував кілька запитів. updateOn: 'blur' обмежує запуск валідатора моментом виходу з поля. Якщо API недоступний, catchError повертає null, форма не застрягає. Нюанс: якщо викликати patchValue() на цьому контролі, асинхронний валідатор не перезапуститься автоматично. Виклич updateValueAndValidity() явно, якщо потрібна примусова перевірка.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?