Reactive Forms — pierwsze spotkanie
Wszystkie dotychczasowe formularze opisywał szablon: atrybuty required, minlength, [(ngModel)]. Angular ma też drugą drogę — Reactive Forms — w której cały formularz, z polami i regułami, opisujesz w klasie. Poznasz ją na tyle, żeby rozpoznać ją w cudzym kodzie i wiedzieć, kiedy po nią sięgnąć.
Czego się dziś nauczysz
- Utworzysz pole
FormControli połączysz je z elementeminputprzez[formControl]. - Odczytasz i ustawisz wartość pola:
.valuei.setValue(). - Zbudujesz formularz
FormGroupi połączysz go z szablonem przez[formGroup]iformControlName. - Dodasz walidatory
Validatorsw kodzie i wyświetlisz błędy tak jak w lekcji 26. - Wyślesz formularz, zapiszesz jego wartość na liście i przywrócisz stan początkowy przez
reset(). - Porównasz formularz oparty na szablonie z reaktywnym i wybierzesz właściwy do zadania.
Przygotowanie: lekcje 17–27 — formularze z ngModel, walidacja i wysyłka. Przewidywany czas: 45 min z zadaniami.
Formularz w klasie zamiast w szablonie
W formularzu z ngModel reguły są rozsiane po HTML-u: tu required, tam pattern. Przy pięciu polach to wygodne. Przy dwudziestu polach, regułach zależnych od innych pól i testach automatycznych — robi się trudne do ogarnięcia. W Reactive Forms tworzysz w klasie obiekt formularza: listę pól, ich wartości początkowe i walidatory. Szablon tylko wskazuje, które pole HTML odpowiada któremu polu w klasie.
| Element | Co to jest |
|---|---|
ReactiveFormsModule | moduł do imports komponentu — zamiast FormsModule |
new FormControl('') | jedno pole z wartością początkową; ma .value, .valid, .errors, .setValue() |
new FormGroup({ … }) | formularz złożony z nazwanych pól |
Validators.required, .minLength(3), .email, .min(1), .pattern(…) | te same reguły co atrybuty z lekcji 26, zapisane w kodzie |
[formControl]="pole" | łączy pojedynczy input z polem z klasy |
[formGroup]="formularz" + formControlName="imie" | łączy <form> z grupą, a każde pole — z polem grupy o tej nazwie |
Nie mieszaj dróg
W Reactive Forms nie używasz [(ngModel)] ani sygnałów dla pól. Wartość mieszka w obiekcie formularza: formularz.value. Jeden formularz — jedna droga.
Jedno pole: FormControl
Jedno pole w klasie, połączone z input przez [formControl]. Przycisk ustawia wartość z kodu metodą setValue.
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [ReactiveFormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly miasto = new FormControl('');
lebork(): void {
this.miasto.setValue('Lębork');
}
}
<div class="p-3" style="width: 24rem">
<label class="form-label" for="miasto">Miasto</label>
<input id="miasto" class="form-control mb-2" [formControl]="miasto">
<button class="btn btn-outline-primary" type="button" (click)="lebork()">Wpisz Lębork</button>
<p class="mt-3">Wartość: „{{ miasto.value }}”</p>
</div>
[Lębork ]
Wartość: „Lębork”
Działa jak [(ngModel)] z lekcji 17: pisanie zmienia napis, przycisk wypełnia pole. Różnica jest w klasie — zamiast signal('') jest new FormControl(''), a odczyt to miasto.value, bez nawiasów.
Zatrzymaj się
miasto.value nie ma nawiasów, a miasto.setValue(…) ma. Czym różni się odczyt właściwości od wywołania metody?
Formularz z kilku pól: FormGroup
Grupa trzech pól o nazwach imie, klasa i zgoda. W szablonie [formGroup] wskazuje grupę, a formControlName — pole w grupie. Wartość całego formularza to jeden obiekt.
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-demo',
imports: [ReactiveFormsModule, JsonPipe],
templateUrl: './demo.html'
})
export class Demo {
readonly formularz = new FormGroup({
imie: new FormControl(''),
klasa: new FormControl('2TP'),
zgoda: new FormControl(false),
});
}
<div class="p-3" style="width: 26rem">
<form [formGroup]="formularz">
<input class="form-control mb-2" placeholder="Imię" formControlName="imie">
<select class="form-select mb-2" formControlName="klasa">
<option value="1TP">1TP</option>
<option value="2TP">2TP</option>
<option value="3TP">3TP</option>
</select>
<div class="form-check">
<input id="zgoda" class="form-check-input" type="checkbox" formControlName="zgoda">
<label class="form-check-label" for="zgoda">Zgoda rodzica</label>
</div>
</form>
<pre class="bg-light p-2 mt-3 small" style="width: 26rem">{{ formularz.value | json }}</pre>
</div>
{
"imie": "",
"klasa": "2TP",
"zgoda": false
}
Każda zmiana w dowolnym polu od razu zmienia ten obiekt. Nazwy kluczy pochodzą z klasy, nie z atrybutów name.
Zatrzymaj się
Porównaj z f.value z lekcji 24. Skąd tam brały się nazwy kluczy, a skąd tutaj?
Walidatory w kodzie: Validators
Te same reguły co w lekcji 26, tylko zapisane przy polach w klasie. Szablon wyświetla błędy dokładnie tak jak wcześniej — z is-invalid i invalid-feedback. Do pola sięgamy przez formularz.controls.nazwa; w klasie zapisaliśmy skróty.
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [ReactiveFormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly formularz = new FormGroup({
login: new FormControl('', [Validators.required, Validators.minLength(4)]),
email: new FormControl('', [Validators.required, Validators.email]),
wiek: new FormControl<number | null>(null, [Validators.required, Validators.min(13), Validators.max(19)]),
});
readonly login = this.formularz.controls.login;
readonly email = this.formularz.controls.email;
readonly wiek = this.formularz.controls.wiek;
}
<div class="p-3" style="width: 26rem">
<form [formGroup]="formularz">
<div class="mb-3">
<label class="form-label" for="login">Login *</label>
<input id="login" class="form-control" formControlName="login"
[class.is-invalid]="login.invalid && login.touched">
<div class="invalid-feedback">
@if (login.errors?.['required']) { Login jest obowiązkowy. }
@if (login.errors?.['minlength']) { Login musi mieć co najmniej 4 znaki. }
</div>
</div>
<div class="mb-3">
<label class="form-label" for="email">E-mail *</label>
<input id="email" type="email" class="form-control" formControlName="email"
[class.is-invalid]="email.invalid && email.touched">
<div class="invalid-feedback">Podaj poprawny adres e-mail.</div>
</div>
<div class="mb-3">
<label class="form-label" for="wiek">Wiek *</label>
<input id="wiek" type="number" class="form-control" formControlName="wiek"
[class.is-invalid]="wiek.invalid && wiek.touched">
<div class="invalid-feedback">Wiek od 13 do 19 lat.</div>
</div>
<p class="small text-secondary">Formularz poprawny: {{ formularz.valid }}</p>
</form>
</div>
login „ab” i Tab: Login musi mieć co najmniej 4 znaki.
wiek 25 i Tab: Wiek od 13 do 19 lat.
po poprawnych danych: Formularz poprawny: true
W HTML-u nie ma ani jednego atrybutu walidacji — wszystkie reguły są w klasie. Uwaga na wielkość liter: atrybut w HTML-u to minlength, walidator w kodzie — Validators.minLength, a klucz w errors — zawsze 'minlength'.
Zatrzymaj się
Pola login, email i wiek w klasie to tylko skróty do formularz.controls…. Jak wyglądałby szablon bez nich?
Wysyłka, lista i reset()
Pełny cykl w wersji reaktywnej: (ngSubmit) na formularzu, sprawdzenie invalid, zapis getRawValue() na listę i reset(), który przywraca wartości początkowe i czyści stan „dotknięcia”.
import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
interface Zapis {
nick: string;
gra: string;
}
@Component({
selector: 'app-demo',
imports: [ReactiveFormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly gry = ['Tetris', 'Pac-Man', 'Bomberman'];
readonly lista = signal<Zapis[]>([]);
readonly formularz = new FormGroup({
nick: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(3)] }),
gra: new FormControl('Tetris', { nonNullable: true }),
});
zapisz(): void {
if (this.formularz.invalid) {
this.formularz.markAllAsTouched();
return;
}
this.lista.update(l => [...l, this.formularz.getRawValue()]);
this.formularz.reset();
}
}
<div class="p-3" style="width: 26rem">
<form [formGroup]="formularz" (ngSubmit)="zapisz()">
<div>
<input class="form-control" placeholder="Nick (min. 3 znaki)" formControlName="nick"
[class.is-invalid]="formularz.controls.nick.invalid && formularz.controls.nick.touched">
<div class="invalid-feedback">Nick musi mieć co najmniej 3 znaki.</div>
</div>
<select class="form-select mt-2" formControlName="gra">
@for (g of gry; track g) { <option [value]="g">{{ g }}</option> }
</select>
<button class="btn btn-primary mt-2" type="submit">Zapisz</button>
</form>
<ul class="list-group mt-3" style="width: 26rem">
@for (z of lista(); track $index) {
<li class="list-group-item">{{ z.nick }} — {{ z.gra }}</li>
} @empty {
<li class="list-group-item text-secondary">Brak zapisów.</li>
}
</ul>
</div>
Pixel — Pac-Man
(formularz: pusty nick, Tetris, bez czerwonej ramki)
nonNullable: true sprawia, że reset() przywraca wartość początkową ('', 'Tetris'), a nie null. getRawValue() zwraca wartości wszystkich pól jako obiekt. Lista jest zwykłym sygnałem — Reactive Forms dotyczą tylko pól formularza.
Zatrzymaj się
W liście @for jest [value]="g", a nie [ngValue]. Dlaczego tutaj wystarczy [value]?
Zestawienie elementów
| Formularz z ngModel (lekcje 17–27) | Reactive Forms | Uwagi |
|---|---|---|
FormsModule | ReactiveFormsModule | Z @angular/forms. |
signal('') + [(ngModel)] | new FormControl('') + [formControl] | Wartość: .value. |
#f="ngForm", name="…" | [formGroup], formControlName="…" | Klucze z klasy. |
required minlength="4" | Validators.required, Validators.minLength(4) | Klucz błędu ten sam. |
required na checkboxie | Validators.requiredTrue | Musi być zaznaczony. |
#x="ngModel" → x.invalid | formularz.controls.x.invalid | Te same właściwości. |
ręczne set wartości początkowych | formularz.reset() | Z nonNullable: true. |
sygnal.set(x) | pole.setValue(x) | Ustawienie z kodu. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
[formControl] bez ReactiveFormsModule | NG8002: Can’t bind to 'formControl’ since it isn’t a known property of 'input’. |
formControlName poza <form [formGroup]> | Błąd w konsoli: formControlName must be used with a parent formGroup directive. |
Literówka: formControlName="imei" | Błąd w konsoli: Cannot find control with name: 'imei’. Nazwa musi zgadzać się z kluczem w FormGroup. |
[(ngModel)] i formControlName na tym samym polu | Ostrzeżenie w konsoli, że takie połączenie jest przestarzałe. Wybierz jedną drogę. |
reset() i null w polu | Bez nonNullable: true reset ustawia pola na null, a nie na wartości początkowe. |
{{ miasto.value() }} | Błąd — value to właściwość, nie sygnał. Bez nawiasów. |
Zadania
Pole FormControl z kolorem ulubionej drużyny (type="color") i kwadrat w tym kolorze obok. Przycisk „Czerwony” ustawia kolor #dc3545.
Kryterium sukcesu: zmiana koloru w próbniku i przycisk od razu zmieniają kwadrat.
Podpowiedź
[style.background-color]="kolor.value" i kolor.setValue('#dc3545').
Zbuduj FormGroup z polami tytul, sala (liczba) i godzina (time) i pokaż pod formularzem kartę ogłoszenia zbudowaną z formularz.value.
Kryterium sukcesu: karta zmienia się przy każdej zmianie w dowolnym polu.
Podpowiedź
{{ formularz.value.tytul }} — wartość grupy to zwykły obiekt.
Weź formularz z przykładu 4 lekcji 26 (imię, telefon, regulamin) i przepisz go na Reactive Forms.
Kryterium sukcesu: zachowanie jest identyczne z wersją z lekcji 26, a w HTML-u nie ma atrybutów walidacji.
Podpowiedź
Validators.required, Validators.minLength(5), Validators.pattern('[0-9]{9}'), Validators.requiredTrue dla checkboxa.
Formularz zgłoszenia na turniej (nick min. 3 znaki, klasa z listy, rozmiar koszulki — radio) w Reactive Forms. Zapisane zgłoszenia trafiają na listę, a formularz wraca do wartości początkowych. Pod formularzem przycisk „Wypełnij przykładem”, który ustawia wszystkie pola naraz.
Kryterium sukcesu: zapis działa tylko dla poprawnych danych; po zapisie formularz jest czysty i bez czerwieni; przycisk przykładowy wypełnia trzy pola jednym kliknięciem.
Podpowiedź
formularz.setValue({ nick: 'Pixel', klasa: '2TP', rozmiar: 'M' }) — obiekt musi mieć wszystkie klucze. Radio: formControlName na każdym przycisku z tą samą nazwą.
Przygotuj tabelkę (w komentarzu w kodzie albo na kartce): pięć różnic między formularzem z ngModel a Reactive Forms. Potem napisz, którą drogę wybrałbyś do: a) formularza kontaktowego z trzema polami, b) ankiety z 30 pytaniami, gdzie część pytań zależy od wcześniejszych odpowiedzi.
Kryterium sukcesu: tabelka zawiera co najmniej pięć konkretnych różnic, a wybór w obu przypadkach ma uzasadnienie.
Podpowiedź
Pomyśl o tym, gdzie mieszkają reguły, gdzie wartość, jak się testuje, jak się dodaje pola w trakcie działania i ile kodu trzeba napisać na start.
Przepisz krok 3 warsztatu z lekcji 27 (formularz zapisu z walidacją) na Reactive Forms. Koszt nadal ma się liczyć na bieżąco.
Kryterium sukcesu: walidacja, zapis i czyszczenie działają jak w lekcji 27; koszt zmienia się przy każdej zmianie pól.
Podpowiedź
Koszt policz metodą koszt(), która czyta this.formularz.getRawValue() — wywołana w szablonie przelicza się po każdym zdarzeniu z formularza. reset() zastąpi metodę wyczysc.
Co trzeba zapamiętać
- Reactive Forms opisują formularz w klasie:
FormControl,FormGroup,Validators; wymagająReactiveFormsModule. - W szablonie łączysz pola przez
[formControl]albo[formGroup]iformControlName. - Wartość pola to
.value(bez nawiasów), ustawienie —.setValue(). - Wartość całego formularza to
formularz.valuealbogetRawValue();reset()przywraca stan początkowy. - Do prostych formularzy wystarczy
ngModel; Reactive Forms opłacają się przy dużych i dynamicznych formularzach.
Sprawdź się: w cudzym projekcie widzisz formControlName="email". Gdzie szukasz reguł walidacji tego pola i jakiego importu się spodziewasz?
Dokumentacja: angular.dev — Reactive Forms.