Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Різниця між формами, керованими шаблоном, та реактивними формами в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Форми на основі шаблонів** використовують `ngModel` в HTML, Angular будує модель форми автоматично. **Реактивні форми (reactive forms)** описують модель явно в TypeScript через `FormGroup`. ```typescript form = new FormGroup({ email: new FormControl('', [Validators.required, Validators.email]) }); ``` **Ключове:** Реактивні форми дають програмний доступ до кожного контролу через RxJS, підтримують додавання полів під час роботи через `addControl()` і простіше тестуються. Форми на основі шаблонів підходять для простих статичних форм без динамічної логіки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Форми на основі шаблонів і реактивні форми (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-driven | Reactive | |---|---|---| | Налаштування | `FormsModule` + `ngModel` в HTML | `ReactiveFormsModule` + `FormGroup` у TS | | Потік даних | Двостороннє зв'язування (`ngModel`) | Однонаправлений (`valueChanges` observable) | | Валідація | Атрибути HTML + помилки директив | Масив `Validators` на `FormControl` | | Динамічні поля | Обхідні шляхи через `*ngFor` | `addControl()` / `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()` явно, якщо потрібна примусова перевірка.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.