Dataczwartek, 13 sierpnia 2026 Czas18:41:35
← Aplikacje webowe

Lekcja 11. Angular III — formularze reaktywne, walidacja i routing SPA

Trudny / egzaminacyjny

Po 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

  1. Dodaj do formularza trzecie pole "Kategoria" (proste <select> z kilkoma opcjami wpisanymi na sztywno w HTML) z walidatorem Validators.required.

  2. Zbuduj trasę z parametrem (produkty/:id) i komponent SzczegolyProduktuComponent odczytujący ten parametr przez ActivatedRoute — dodaj link do niej z listy produktów, przekazujący konkretne Id klikniętego produktu.

  3. Zaimplementuj WŁASNY walidator (funkcja zgodna z interfejsem ValidatorFn z @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.