Lekcja 11. Angular III — formularze reaktywne, walidacja i routing SPA
Trudny / egzaminacyjnyPo co się tego uczymy?
Ostatni element układanki Angulara: prawdziwe formularze z walidacją (nie tylko proste [(ngModel)] z lekcji 10) oraz nawigacja między "stronami" aplikacji jednostronicowej (SPA) — bez przeładowania całej strony w przeglądarce. Po tej lekcji będziesz mieć komplet wiedzy potrzebnej do zbudowania w pełni funkcjonalnej aplikacji Angular, gotowej do połączenia z backendem ASP.NET Core w kolejnych lekcjach.
Teoria
Dwa podejścia do formularzy w Angularze. [(ngModel)] z lekcji 10 (tzw. "formularze szablonowe") sprawdza się w prostych przypadkach, ale profesjonalne, większe formularze buduje się przez Reactive Forms — podejście, gdzie CAŁA struktura formularza (pola, reguły walidacji) jest zdefiniowana w KODZIE TypeScript, a szablon HTML tylko się do niej "podłącza". To podejście jest bardziej przewidywalne i łatwiejsze do testowania w większych aplikacjach.
Budowa formularza reaktywnego. Klasa FormGroup reprezentuje CAŁY formularz, a FormControl — POJEDYNCZE pole, z opcjonalną wartością początkową i tablicą walidatorów:
import { FormGroup, FormControl, Validators } from '@angular/forms';
formularz = new FormGroup({
nazwa: new FormControl('', [Validators.required, Validators.minLength(2)]),
cena: new FormControl(0, [Validators.required, Validators.min(0.01)])
});
W szablonie HTML formularz "podłącza się" atrybutem [formGroup]="formularz" na znaczniku <form>, a każde pole atrybutem formControlName="nazwa". Sprawdzenie poprawności: formularz.valid (cały formularz), formularz.get('nazwa')?.invalid (pojedyncze pole).
Wbudowane walidatory Angulara (klasa Validators) — bezpośredni odpowiednik Data Annotations z lekcji o ASP.NET Core:
| Walidator Angular | Odpowiednik w C# (Data Annotations) |
|---|---|
Validators.required |
[Required] |
Validators.minLength(n) / maxLength(n) |
[StringLength] |
Validators.min(n) / max(n) |
[Range] |
Validators.email |
[EmailAddress] |
Validators.pattern(regex) |
własny atrybut walidacyjny z wyrażeniem regularnym |
Walidacja HTML5 (natywna przeglądarki). Niezależnie od Angulara, sam HTML5 oferuje podstawową walidację przez atrybuty znaczników: required, type="email", min/max, pattern — przeglądarka SAMA blokuje wysłanie formularza i pokazuje wbudowany komunikat, jeśli te warunki nie są spełnione. To dobre uzupełnienie (pierwsza linia obrony), ale walidacja Angulara (Reactive Forms) daje PEŁNĄ kontrolę nad wyglądem komunikatów i logiką.
Routing — nawigacja SPA. Plik app.routes.ts definiuje mapowanie adresów URL na komponenty: { path: 'produkty', component: ProduktyComponent }. Znacznik <router-outlet></router-outlet> w głównym komponencie aplikacji wskazuje MIEJSCE, gdzie ma się pojawić aktualnie aktywny komponent-strona — dokładnie ten sam mechanizm co @RenderBody() w Blazor albo @Body w layoucie. Nawigacja z poziomu szablonu: <a routerLink="/produkty">Produkty</a>; z poziomu kodu: wstrzyknięty Router i wywołanie this.router.navigate(['/produkty']).
Parametry w adresie. Trasa { path: 'produkty/:id', component: SzczegolyProduktuComponent } pozwala odebrać wartość przez wstrzyknięty ActivatedRoute: this.route.snapshot.paramMap.get('id') — koncepcyjnie identyczne do parametrów adresu w Razor Pages/Blazor z wcześniejszych lekcji tego kursu.
Schemat
Reactive Forms:
FormGroup (cały formularz)
├── FormControl "nazwa" [Validators.required, minLength(2)]
└── FormControl "cena" [Validators.required, min(0.01)]
│
│ formularz.valid?
niepoprawny ──┴── poprawny
│ │
▼ ▼
pokaż błędy onSubmit() → wyślij dane dalej
Routing SPA:
app.routes.ts:
{ path: "", component: StronaGlownaComponent }
{ path: "produkty", component: ProduktyComponent }
{ path: "produkty/:id", component: SzczegolyProduktuComponent }
app.component.html:
<nav>
<a routerLink="/">Start</a>
<a routerLink="/produkty">Produkty</a>
</nav>
<router-outlet></router-outlet> ← TU wstawiany aktywny komponent
Kliknięcie linku "/produkty" → Router podmienia zawartość <router-outlet>
BEZ przeładowania całej strony (charakterystyczne dla SPA)
Przykład z życia
Formularz rejestracji nowego produktu w panelu sprzedawcy pokazuje na bieżąco, czy nazwa jest wystarczająco długa i czy cena jest liczbą dodatnią, dokładnie tak jak w Data Annotations z ASP.NET Core — a menu z linkami "Strona główna / Produkty / Kontakt" przełącza widoczną treść błyskawicznie, bez przeładowania całej strony, bo to jedna aplikacja Angular obsługująca kilka "widoków".
dodaj-produkt.component.html
<!-- dodaj-produkt.component.html -->
<form [formGroup]="formularz" (ngSubmit)="zapisz()" class="container mt-4">
<div class="mb-3">
<label class="form-label">Nazwa produktu</label>
<input formControlName="nazwa" class="form-control" />
@if (formularz.get('nazwa')?.invalid && formularz.get('nazwa')?.touched) {
<div class="text-danger">Nazwa musi mieć co najmniej 2 znaki.</div>
}
</div>
<div class="mb-3">
<label class="form-label">Cena</label>
<input type="number" formControlName="cena" class="form-control" />
@if (formularz.get('cena')?.invalid && formularz.get('cena')?.touched) {
<div class="text-danger">Cena musi być liczbą dodatnią.</div>
}
</div>
<button type="submit" class="btn btn-primary" [disabled]="formularz.invalid">Zapisz</button>
</form>
dodaj-produkt.component.ts + app.routes.ts
// dodaj-produkt.component.ts
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
@Component({
selector: 'app-dodaj-produkt',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './dodaj-produkt.component.html'
})
export class DodajProduktComponent {
formularz = new FormGroup({
nazwa: new FormControl('', [Validators.required, Validators.minLength(2)]),
cena: new FormControl(0, [Validators.required, Validators.min(0.01)])
});
constructor(private router: Router) {}
zapisz() {
if (this.formularz.invalid) {
return;
}
console.log('Zapisano produkt:', this.formularz.value);
this.router.navigate(['/produkty']);
}
}
// ---------------------------------------------------------------------
// app.routes.ts
import { Routes } from '@angular/router';
import { ProduktyComponent } from './produkty/produkty.component';
import { DodajProduktComponent } from './dodaj-produkt/dodaj-produkt.component';
export const routes: Routes = [
{ path: 'produkty', component: ProduktyComponent },
{ path: 'dodaj-produkt', component: DodajProduktComponent }
];
Komentarz i wyjaśnienie kodu
formularz.get('nazwa')?.touched sprawdza, czy użytkownik JUŻ dotknął (kliknął, a potem opuścił) to pole — dzięki temu komunikat błędu NIE pojawia się natychmiast przy otwarciu pustego formularza (co wyglądałoby nachalnie), tylko dopiero po tym, jak użytkownik faktycznie wszedł w interakcję z polem i np. zostawił je puste.
[disabled]="formularz.invalid" na przycisku "Zapisz" to dodatkowa, WIZUALNA warstwa ochrony — przycisk jest wyszarzony/nieaktywny, dopóki CAŁY formularz nie jest poprawny; to NIE zastępuje jednak sprawdzenia if (this.formularz.invalid) return; w metodzie zapisz(), bo teoretycznie formularz mógłby zostać wysłany innym sposobem (np. klawiszem Enter).
this.router.navigate(['/produkty']) po udanym zapisie to bezpośredni odpowiednik RedirectToPage(...) z Razor Pages i NavigationManager.NavigateTo(...) z Blazor — programowe przejście na inny "widok" aplikacji po zakończeniu operacji.
Ćwiczenie samodzielne
Zbuduj formularz reaktywny z dwoma polami i walidacją jak w przykładzie, oraz skonfiguruj podstawowy routing z co najmniej dwiema trasami. Sprawdź w przeglądarce: czy przycisk "Zapisz" jest zablokowany przy pustych/niepoprawnych polach, i czy nawigacja między trasami działa BEZ przeładowania całej strony (obserwuj pasek adresu i to, czy cała strona "mruga" przy przeładowaniu).
Zadania do pracy własnej
Dodaj do formularza trzecie pole "Kategoria" (proste
<select>z kilkoma opcjami wpisanymi na sztywno w HTML) z walidatoremValidators.required.Zbuduj trasę z parametrem (
produkty/:id) i komponentSzczegolyProduktuComponentodczytujący ten parametr przezActivatedRoute— dodaj link do niej z listy produktów, przekazujący konkretne Id klikniętego produktu.Zaimplementuj WŁASNY walidator (funkcja zgodna z interfejsem
ValidatorFnz@angular/forms) sprawdzający niestandardową regułę biznesową — np. że nazwa produktu nie może zawierać wyłącznie cyfr — i zastosuj go razem z wbudowanymi walidatorami Angulara na tym samym polu formularza.
Typowe błędy
Poleganie WYŁĄCZNIE na atrybucie [disabled] przycisku jako jedynym zabezpieczeniu — zawsze dodatkowo sprawdzaj formularz.invalid na początku metody obsługującej zapis, niezależnie od stanu przycisku w interfejsie.
Zapominanie o zaimportowaniu ReactiveFormsModule w tablicy imports standalone komponentu korzystającego z [formGroup]/formControlName — bez tego Angular nie rozpozna tych atrybutów.
Mylenie routerLink (nawigacja Angulara, BEZ przeładowania strony) ze zwykłym href — użycie zwykłego <a href="/produkty"> zamiast routerLink spowoduje PEŁNE przeładowanie całej aplikacji przez przeglądarkę, tracąc całą zaletę SPA.
Nawiązanie do egzaminu zawodowego
To bezpośrednia realizacja INF.04.7.3 ("mechanizmy walidacji formularzy za pomocą HTML5" oraz "dynamiczne formularze") i domyka wiedzę o Angularze potrzebną do budowy pełnoprawnej aplikacji SPA. Od kolejnej lekcji przechodzimy do DRUGIEJ połowy tego działu — jak ASP.NET Core dostarcza dane, z których korzysta cały ten frontend.