Kurs Angular · Moduł 8: Formularze
Formularze, które słuchają
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. Jaka jest różnica między setValue() a patchValue()?
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).