Модуль 22

Reactive Forms та FormBuilder

Ось готовий урок, створений у стилі CS50, спеціально для теми Reactive Forms та FormBuilder в Angular.


🎓 CS50: Reactive Forms та FormBuilder (Angular)

Вітаю, друзі! Це... ні, це не Гарвард, але ми тут, щоб розібратися з однією з найпотужніших концепцій у світі Frontend-розробки.

Мене звуть [Ваше ім’я / ШІ-Інструктор], і сьогодні ми поговоримо про контроль. Тотальний контроль над даними.


1. 🔥 Вступ: Хаос у формах

Уявіть собі ситуацію. Ви пишете форму реєстрації для нового Facebook або TikTok. У вас є поля: логін, пароль, повтор пароля, адреса, чекбокс "Я згоден продати душу"...

Спочатку все просто. Ви кидаєте input в HTML, вішаєте [(ngModel)] (якщо ви вже знайомі з template-driven підходом), і все працює.

Але раптом приходить менеджер і каже:

"Слухай, зроби так, щоб поле 'Адреса' зникало, якщо користувач не поставив галочку 'Хочу доставку'. А якщо пароль коротший за 8 символів, кнопка 'Надіслати' має бути червоною. І ще, як тільки він вводить email, ми маємо фоново перевіряти, чи він вільний, не чекаючи натискання кнопки."

Ви починаєте ліпити купу *ngIf, купу змінних у класі, купу обробників подій... Ваш код перетворюється на спагеті. 🍝

Риторичне запитання: Чи не було б круто, якби у вас був пульт керування, де вся логіка форми жила б в одному місці (у вашому TypeScript коді), а HTML був би просто "дурним" екраном, який це показує?

Ось навіщо нам Reactive Forms. Це не просто спосіб отримати дані. Це спосіб керувати станом форми програно, чітко і реактивно.

Аналогія: * Template-driven форми — це як заповнювати паперову анкету в держустанові. Ви пишете, що хочете, а помилку помітять, тільки коли ви віддасте листок у віконце. * Reactive Forms — це як розумний інтерфейс Джарвіса у Залізної Людини. Система стежить за кожним вашим рухом у реальному часі, підказує, блокує або дозволяє дії миттєво.


2. 🧠 Теоретична база (Що під капотом?)

Давайте заглянемо під капот. У Reactive Forms ми не покладаємося на HTML. Ми створюємо модель форми у коді компонента.

Тут є три головні герої, яких треба знати в обличчя:

1. FormControl (Атом) ⚛️

Це найдрібніша частинка. Одне поле введення. * Він знає своє значення (наприклад, "admin"). * Він знає свій статус (VALID, INVALID, PENDING). * Він знає, чи його торкалися (touched/untouched).

2. FormGroup (Молекула) 🧬

Це контейнер для FormControl-ів. * Якщо у вас є форма логіна (email + пароль), це — група. * Магія: Якщо хоч один контрол всередині INVALID, то вся група автоматично стає INVALID.

3. FormBuilder (Архітектор) 👷‍♂️

Це сервіс-помічник. * Створювати контрол так: new FormControl('', Validators.required) і групувати їх вручну — це довго і нудно. * FormBuilder — це "синтаксичний цукор", який дозволяє писати менше коду, але робити те саме.

💡 Що зрозуміти інтуїтивно:

У Reactive Forms джерело правди — це TypeScript код. HTML-шаблон просто "підписується" на ваш код. Змінили код — змінився шаблон. Змінили щось у шаблоні — код про це дізнався миттєво через потоки (Streams/RxJS).


3. 🧪 Приклади (Step-by-step)

Приклад 1: "Хардкорний" спосіб (Без FormBuilder)

Спочатку подивіться, як це робиться "в лоб", щоб зрозуміти, навіщо нам спрощення.

// component.ts
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({...})
export class MyFormComponent {
  // Ми створюємо інстанси класів вручну
  loginForm = new FormGroup({
    email: new FormControl('', Validators.required),
    password: new FormControl('')
  });

  onSubmit() {
    console.log(this.loginForm.value);
  }
}

В HTML ми просто "зв'язуємо" їх:

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
  <input formControlName="email" placeholder="Email">
  <input formControlName="password" placeholder="Password">
  <button type="submit">Увійти</button>
</form>

Питання до вас: Виглядає непогано, так? Але що, якщо у нас 20 полів? Писати new FormControl(...) 20 разів? В очах зарябіє від слова new.


Приклад 2: Елегантний спосіб (З FormBuilder) 🎩

Ось тут виходить на сцену наш герой — FormBuilder.

// component.ts
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms'; // Імпортуємо FormBuilder

@Component({...})
export class BetterFormComponent {
  private fb = inject(FormBuilder); // Інжектуємо сервіс (або через конструктор)

  // Дивіться, як чисто! Жодних "new FormControl"
  profileForm = this.fb.group({
    username: ['', Validators.required], // [значення, валідатори]
    bio: [''],
    address: this.fb.group({ // Вкладена група!
      city: ['Kyiv'],
      street: ['']
    })
  });

  save() {
    // Ми можемо перевірити валідність усієї форми одним махом
    if (this.profileForm.valid) {
      console.log('Дані готові до відправки:', this.profileForm.value);
    } else {
      console.log('Форма невалідна!');
    }
  }
}

Що ми отримали? 1. Код читається як звичайна структура об'єкта. 2. Легко робити вкладеність (адреса всередині профілю). 3. Валідація додається другим параметром у масиві.


4. 🛠 Практична частина

Прийшов час забруднити руки кодом. Відкривайте IDE (або StackBlitz).

🔹 Завдання 1: Створіть форму замовлення піци

Використовуйте FormBuilder. Поля: 1. size (Розмір: "Маленька", "Велика"). За замовчуванням "Велика". 2. topping (Начинка). 3. contactInfo (Вкладена група): * phone (обов'язкове поле). * address.

🔹 Завдання 2: Реакція на зміни

У Reactive Forms ми можемо підписатися на зміни! Додайте в ngOnInit такий код (адаптуйте під свою форму):

this.pizzaForm.get('size')?.valueChanges.subscribe(val => {
  console.log(`Ви змінили розмір піци на: ${val}`);
});

Мета: Побачити в консолі повідомлення кожного разу, коли ви змінюєте розмір.

🔹 Завдання 3: "А що, якщо..." (Динамічне керування)

Зробіть так, щоб поле address було обов'язковим (Validators.required), тільки якщо обрано розмір "Велика". Підказка: Вам потрібно слухати valueChanges поля size і використовувати методи setValidators або clearValidators для поля address.

🔹 Завдання 4: Міні-кейс "Кнопка Reset"

Додайте кнопку "Очистити". При кліку форма має скинутися до початкового стану (наприклад, розмір знову має стати "Велика", а не порожнім). Підказка: Метод .reset({ size: 'Велика' }).


5. 💡 Мислення як у розробника

Як відрізнити новачка від профі в Angular-формах?

1. Типізація (Strict Typed Forms) Новачки часто працюють з формами як з any. Профі знає, що з Angular 14+ форми строго типізовані.

// Замість загального FormGroup, Angular сам виводить тип:
profileForm = this.fb.group({
  name: new FormControl<string | null>(null)
});
// Тепер this.profileForm.value.name — це точно string або null, а не будь-що.

2. Не мішайте стилі Помилка: Використовувати [(ngModel)] всередині форми, яка вже керується [formGroup]. Як треба: Якщо ви обрали Reactive Forms — використовуйте formControlName. Забудьте про ngModel.

3. Логіка в класі, а не в шаблоні Якщо ви пишете в HTML щось типу <div *ngIf="form.get('email').invalid && form.get('email').touched">, це ок для навчання. Профі винесе це в гетер:

get isEmailInvalid() {
  const email = this.profileForm.get('email');
  return email?.invalid && email?.touched;
}

І в HTML буде просто: *ngIf="isEmailInvalid". Чистота — запорука здоров'я коду!


6. 🧩 Підсумок

Отже, що ми маємо сьогодні в сухому залишку?

  1. Reactive Forms дають нам повний контроль над даними в компоненті (TypeScript), а не в шаблоні (HTML).
  2. FormControl — це одиниця даних.
  3. FormGroup — це група одиниць.
  4. FormBuilder — це наш найкращий друг, який економить час на написанні коду.
  5. Тепер ви можете підписуватися на зміни полів (valueChanges) як на стрім YouTube!

Ви тепер вмієте: Створювати складні, вкладені форми, валідувати їх і реагувати на зміни користувача в реальному часі.

🚀 Тизер наступного уроку

Але стривайте! А що, якщо користувач хоче додати не одну піцу, а п'ять? Або десять? Ми ж не будемо створювати pizza1, pizza2, pizza3 вручну? На наступному уроці ми розберемо FormArray — спосіб динамічно додавати та видаляти поля форми прямо під час роботи програми.

А поки що — це був CS50... тобто, курс по Angular. Кодьте із задоволенням! 👋