Przejdź do treści
School IT / Przestrzeń do nauki

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ąć.

Angular 22 ReactiveFormsModule FormControl FormGroup Validators formControlName 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz pole FormControl i połączysz je z elementem input przez [formControl].
  • Odczytasz i ustawisz wartość pola: .value i .setValue().
  • Zbudujesz formularz FormGroup i połączysz go z szablonem przez [formGroup] i formControlName.
  • Dodasz walidatory Validators w 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.

TEORIA

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.

ElementCo to jest
ReactiveFormsModulemoduł 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.

PRZYKŁAD 1

Jedno pole: FormControl

Jedno pole w klasie, połączone z input przez [formControl]. Przycisk ustawia wartość z kodu metodą setValue.

src/app/nowa-28-1/demo.ts — pełny plik
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');
    }
}
src/app/nowa-28-1/demo.html — pełny plik
<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>
wynik po kliknięciu „Wpisz Lębork”
[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?

PRZYKŁAD 2

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.

src/app/nowa-28-2/demo.ts — pełny plik
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),
    });
}
src/app/nowa-28-2/demo.html — pełny plik
<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>
ramka na starcie
{
  "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?

PRZYKŁAD 3

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.

src/app/nowa-28-3/demo.ts — pełny plik
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;
}
src/app/nowa-28-3/demo.html — pełny plik
<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>
wynik
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?

PRZYKŁAD 4

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”.

src/app/nowa-28-4/demo.ts — pełny plik
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();
    }
}
src/app/nowa-28-4/demo.html — pełny plik
<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>
wynik po zapisaniu „Pixel / Pac-Man”
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]?

ELEMENTY WBUDOWANE

Zestawienie elementów

Formularz z ngModel (lekcje 17–27)Reactive FormsUwagi
FormsModuleReactiveFormsModuleZ @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 checkboxieValidators.requiredTrueMusi być zaznaczony.
#x="ngModel" → x.invalidformularz.controls.x.invalidTe same właściwości.
ręczne set wartości początkowychformularz.reset()Z nonNullable: true.
sygnal.set(x)pole.setValue(x)Ustawienie z kodu.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
[formControl] bez ReactiveFormsModuleNG8002: 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 poluOstrzeżenie w konsoli, że takie połączenie jest przestarzałe. Wybierz jedną drogę.
reset() i null w poluBez 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

Zadania

ZAD 1Kolor drużyny★☆☆

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').

ZAD 2Podgląd wartości★☆☆

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.

ZAD 3Przepisz lekcję 26★★☆

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.

ZAD 4Formularz z resetem i listą★★☆

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ą.

ZAD 5Która droga?★★★

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.

ZAD 6Warsztat w wersji reaktywnej★★★

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • Reactive Forms opisują formularz w klasie: FormControl, FormGroup, Validators; wymagają ReactiveFormsModule.
  • W szablonie łączysz pola przez [formControl] albo [formGroup] i formControlName.
  • Wartość pola to .value (bez nawiasów), ustawienie — .setValue().
  • Wartość całego formularza to formularz.value albo getRawValue(); 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.

Postęp zapisuje się w tej przeglądarce.