Kurs Angular · Moduł 8: Formularze

Formularze, które słuchają

4 min czytania ·
W tej lekcji4

Zwykły formularz pytasz o wartość, gdy klikniesz "wyślij". Reaktywny formularz działa odwrotnie - to żywe dojo, które melduje każdy ruch na bieżąco. Klucz tej lekcji: nie pytasz formularza o wartość, tylko nasłuchujesz jego zmian. Każde pole ma strumień valueChanges, który płynie jak rzeka - a Ty filtrujesz go operatorami RxJS.

Nasłuchiwanie zmian

Zacznijmy od strumienia valueChanges. Możesz słuchać pojedynczego pola, całego formularza albo jego statusu (poprawny/niepoprawny). Przeczytaj, jak filtruje się taką rzekę.

ngOnInit(): void {
  // Pojedyncze pole - z filtrowaniem
  this.form.get('searchQuery')?.valueChanges.pipe(
    debounceTime(300),          // poczekaj 300ms po ostatnim wpisaniu
    distinctUntilChanged(),      // ignoruj, jeśli wartość się nie zmieniła
    takeUntilDestroyed(this.destroyRef)
  ).subscribe(query => console.log('Szukam:', query));

  // Cały formularz naraz
  this.form.valueChanges.pipe(
    takeUntilDestroyed(this.destroyRef)
  ).subscribe(value => console.log('Formularz:', value));
}

Trzy operatory robią tu całą robotę. debounceTime(300) czeka, aż użytkownik przestanie pisać - bez tego wysyłałbyś zapytanie po każdej literze. distinctUntilChanged pomija powtórki. A takeUntilDestroyed to Twój obowiązkowy strażnik: automatycznie odłącza się od strumienia, gdy komponent znika - bez niego zostałby wyciek pamięci. Zapamiętaj tę trójkę, wraca w każdym reaktywnym formularzu.

Walidatory, które zmieniają się w locie

Czasem reguły zależą od wyboru użytkownika: wybrał kontakt e-mailem - wymagaj e-maila; telefonem - wymagaj numeru. Nasłuchujesz pola i przestawiasz walidatory drugiego pola.

this.form.get('contactMethod')?.valueChanges.pipe(
  takeUntilDestroyed(this.destroyRef)
).subscribe(method => {
  const email = this.form.get('email')!;
  const phone = this.form.get('phone')!;

  if (method === 'email') {
    email.setValidators([Validators.required, Validators.email]);
    phone.clearValidators();
  } else {
    phone.setValidators([Validators.required]);
    email.clearValidators();
  }

  email.updateValueAndValidity();   // bez tego zmiana nie zadziała
  phone.updateValueAndValidity();
});

Zwróć szczególną uwagę na dwie ostatnie linie - to najczęściej zapominany krok. setValidators tylko podmienia reguły, ale nie przelicza statusu pola. Dopiero updateValueAndValidity() każe Angularowi sprawdzić pole od nowa. Pominięcie tego to klasyczny błąd: reguły niby zmienione, a formularz wciąż uparcie pokazuje stary stan.

Most do sygnałów: toSignal

Strumień świetnie się słucha, ale w szablonie wygodniej użyć sygnału. toSignal buduje most: bierze valueChanges i zamienia go w sygnał, który czytasz w szablonie jak każdy inny.

nameSignal = toSignal(
  this.form.get('name')!.valueChanges,
  { initialValue: '' }
);

isValid = toSignal(
  this.form.statusChanges.pipe(map(s => s === 'VALID')),
  { initialValue: false }
);

toSignal przepina strumień na świat sygnałów - odtąd w szablonie piszesz {{ nameSignal() }} zamiast subskrybować ręcznie. initialValue nie jest obowiązkowe, ale bez niego sygnał do pierwszej emisji strumienia zwraca undefined, a jego typ obejmuje undefined; z initialValue sygnał ma wartość od pierwszej chwili. To najczystszy sposób pokazywania stanu formularza w widoku.

Własne pole formularza: ControlValueAccessor

Na koniec technika mistrza: własny widget, który zachowuje się jak wbudowane pole. Gwiazdki ocen mają działać z formControlName jak zwykły input. Umożliwia to interfejs ControlValueAccessor - kontrakt czterech metod, przez które Angular rozmawia z Twoim widżetem.

@Component({
  selector: 'app-star-rating',
  template: `
    @for (star of stars; track $index; let i = $index) {
      <span (click)="setRating(i + 1)" [class.filled]="i < value">★</span>
    }
  `,
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: StarRatingComponent, multi: true }]
})
export class StarRatingComponent implements ControlValueAccessor {
  stars = [1, 2, 3, 4, 5];
  value = 0;
  private onChange: (v: number) => void = () => {};

  writeValue(value: number): void { this.value = value || 0; }
  registerOnChange(fn: (v: number) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void {}

  setRating(rating: number): void {
    this.value = rating;
    this.onChange(rating);   // powiadom formularz o nowej wartości
  }
}

Kontrakt czyta się jak rozmowa w dwie strony. writeValue to formularz mówiący do widżetu "ustaw tę wartość" (gdy dane przychodzą z zewnątrz). registerOnChange daje Ci funkcję, którą wołasz w drugą stronę - onChange(rating) mówi formularzowi "użytkownik wybrał tyle gwiazdek". Rejestracja w NG_VALUE_ACCESSOR sprawia, że Angular traktuje Twój komponent jak natywne pole - i tyle wystarczy, by działał formControlName="rating".

Pamiętaj z tej lekcji jedno: reaktywny formularz to strumień, którego słuchasz, nie pudełko, które odpytujesz. Filtrujesz go operatorami, mostkujesz do sygnałów przez toSignal, a własne pola wpinasz kontraktem ControlValueAccessor.

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaka jest różnica między setValue() a patchValue()?

  2. 2. Której właściwości kontrolki używasz, aby nasłuchiwać zmian jej wartości jako Observable?

To 2 z 5 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij formularz edycji samuraja z ładowaniem danych.

  • Układanie w pionie

    Uporządkuj kroki dynamicznej zmiany walidatorów pola w reakcji na zmianę innego pola (kolejność wykonania, linia po linii).

  • Klikanie w kolejności

    Zbuduj bezpieczną subskrypcję zmian formularza z automatycznym czyszczeniem: this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)). Klikaj elementy po kolei.

  • Układanie w pionie

    Uporządkuj metody interfejsu ControlValueAccessor w typowej kolejności deklaracji w klasie (najpierw zapis wartości, potem rejestracja callbacków).

Przydatne artykuły