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

Walidacja w szablonie

Formularz, który przyjmie pusty e-mail i wiek 250 lat, to formularz do poprawki. Angular ma gotowe walidatory, które włącza się zwykłymi atrybutami HTML: required, minlength, pattern, min, max, email. Twoje zadanie to pokazać błędy w odpowiednim momencie i nie zapisać złych danych.

Angular 22 required minlength pattern min/max email #x=”ngModel” is-invalid 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Włączysz walidatory atrybutami required, minlength, maxlength, pattern, min, max i email.
  • Odczytasz stan pola przez #x="ngModel": invalid, touched, dirty, errors.
  • Pokażesz błąd dopiero po opuszczeniu pola (invalid && touched).
  • Dobierzesz komunikat do rodzaju błędu, odczytując klucz z errors.
  • Użyjesz klas Bootstrapa is-invalid, is-valid i invalid-feedback.
  • Zablokujesz zapis niepoprawnego formularza (f.invalid) i pokażesz wszystkie błędy naraz.

Przygotowanie: lekcje 17–25 — znasz kontrolki, <form>, (ngSubmit) i #f="ngForm". Przewidywany czas: 2 × 45 min z zadaniami.

TEORIA

Dobre dane i dobre komunikaty

W poprzednich lekcjach poprawność sprawdzaliśmy ręcznie w computed: długość, zakres, małpa w adresie. To działa, ale przy dziesięciu polach i kilku warunkach na każde robi się z tego gąszcz. Angular robi to za Ciebie: każde pole z ngModel wie, czy jest poprawne, jakie ma błędy i czy użytkownik już go dotknął.

required nie blokuje wysłania

Gdy w komponencie jest FormsModule, Angular wyłącza wbudowane dymki walidacji przeglądarki. Formularz z pustym polem required i tak się wyśle, a (ngSubmit) się wywoła. To Ty decydujesz, co wtedy zrobić — sprawdzając f.invalid.

AtrybutWarunekKlucz w errors
requiredpole nie może być puste (checkbox — zaznaczony)required
minlength="3"co najmniej 3 znakiminlength z requiredLength, actualLength
maxlength="20"najwyżej 20 znakówzwykle nie wystąpi — przeglądarka nie pozwoli wpisać więcej
pattern="[0-9]{9}"tekst pasuje do wzorupattern
min="13", max="19"zakres liczby (pole number)min / max
emailpoprawny adres e-mailemail
TEORIA

Stan pola i moment pokazania błędu

Zapis #poleImie="ngModel" to zmienna szablonowa, która wskazuje nie na element HTML, ale na kontrolkę Angulara — tę samą dyrektywę NgModel, którą poznałeś w lekcji 17.

WłaściwośćZnaczenie
invalid / validpole ma błędy / jest poprawne
touchedużytkownik wszedł do pola i z niego wyszedł
dirtyużytkownik coś zmienił
errorsobiekt z błędami albo null, gdy ich nie ma

Warunek invalid && touched pokazuje błąd dopiero, gdy użytkownik wyjdzie z pola. Czerwony komunikat przy pustym formularzu, zanim ktokolwiek zaczął pisać, tylko denerwuje. Bootstrap ma gotowe klasy: is-invalid na polu (czerwona ramka) i <div class="invalid-feedback"> zaraz pod polem — komunikat pojawia się sam, gdy pole ma is-invalid.

Bezpieczny dostęp ?.

l.errors?.['required']: gdy errors jest null (brak błędów), całe wyrażenie daje undefined zamiast błędu. Klucz w nawiasach kwadratowych i apostrofach, bo tak się sięga do pola obiektu po nazwie.

PRZYKŁAD 1

Pole obowiązkowe: required

Jedno pole z walidatorem required. Pod polem pokazujemy na żywo stan kontrolki, żebyś widział, kiedy zmieniają się invalid i touched.

src/app/nowa-26-1/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-demo',
    imports: [FormsModule],
    templateUrl: './demo.html'
})
export class Demo {
    readonly imie = signal('');
}
src/app/nowa-26-1/demo.html — pełny plik
<div class="p-3" style="width: 24rem">
  <label class="form-label" for="imie">Imię *</label>
  <input id="imie" name="imie" class="form-control" required
         [(ngModel)]="imie" #poleImie="ngModel"
         [class.is-invalid]="poleImie.invalid && poleImie.touched"
         [class.is-valid]="poleImie.valid && poleImie.touched">
  <div class="invalid-feedback">Podaj imię.</div>

  <p class="small text-secondary mt-3">
    invalid: {{ poleImie.invalid }} · touched: {{ poleImie.touched }} · dirty: {{ poleImie.dirty }}
  </p>
</div>
wynik
na starcie:                   invalid: true · touched: false · dirty: false   (bez czerwieni)
kliknięcie w pole i Tab:      czerwona ramka, „Podaj imię.”
po wpisaniu „Ola”:            zielona ramka

Zatrzymaj się

Zmienna szablonowa nazywa się poleImie, a sygnał imie. Dlaczego nie mogą mieć tej samej nazwy?

PRZYKŁAD 2

Kilka walidatorów i właściwy komunikat

Login: obowiązkowy, 4–12 znaków, tylko małe litery i cyfry. Każdy walidator dodaje do errors własny klucz, więc możemy pokazać dokładnie ten komunikat, który pasuje.

src/app/nowa-26-2/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';

@Component({
    selector: 'app-demo',
    imports: [FormsModule, JsonPipe],
    templateUrl: './demo.html'
})
export class Demo {
    readonly login = signal('');
}
src/app/nowa-26-2/demo.html — pełny plik
<div class="p-3" style="width: 26rem">
  <label class="form-label" for="login">Login *</label>
  <input id="login" name="login" class="form-control"
         required minlength="4" maxlength="12" pattern="[a-z0-9]+"
         [(ngModel)]="login" #l="ngModel"
         [class.is-invalid]="l.invalid && l.touched">

  @if (l.invalid && l.touched) {
    <div class="invalid-feedback">
      @if (l.errors?.['required']) { Login jest obowiązkowy. }
      @if (l.errors?.['minlength']) {
        Za krótki: {{ l.errors?.['minlength'].actualLength }} z {{ l.errors?.['minlength'].requiredLength }} znaków.
      }
      @if (l.errors?.['pattern']) { Dozwolone są tylko małe litery i cyfry. }
    </div>
  }

  <pre class="bg-light p-2 mt-3 small">errors = {{ l.errors | json }}</pre>
</div>
wynik dla kolejnych wpisów
(puste)  → Login jest obowiązkowy.
ab       → Za krótki: 2 z 4 znaków.
Abcd     → Dozwolone są tylko małe litery i cyfry.
abcd     → brak błędu, errors = null

Ramka na dole pokazuje dokładnie, co Angular wie o błędach. Do podglądu potrzebny jest JsonPipe w imports.

Zatrzymaj się

Przy pustym polu widać tylko błąd required, a nie minlength, choć pusty tekst jest krótszy niż 4 znaki. Jak myślisz, dlaczego Angular tak to zaprojektował?

PRZYKŁAD 3

Zakres liczby i adres e-mail

Wiek uczestnika od 13 do 19 lat (min, max na polu liczbowym) i e-mail z walidatorem email. Komunikat przy wieku podaje dozwolony zakres odczytany z błędu.

src/app/nowa-26-3/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-demo',
    imports: [FormsModule],
    templateUrl: './demo.html'
})
export class Demo {
    readonly wiek = signal<number | null>(null);
    readonly email = signal('');
}
src/app/nowa-26-3/demo.html — pełny plik
<div class="p-3" style="width: 26rem">
  <div class="mb-3">
    <label class="form-label" for="wiek">Wiek *</label>
    <input id="wiek" name="wiek" type="number" class="form-control"
           required min="13" max="19" [(ngModel)]="wiek" #w="ngModel"
           [class.is-invalid]="w.invalid && w.touched">
    <div class="invalid-feedback">
      @if (w.errors?.['required']) { Podaj wiek. }
      @if (w.errors?.['min']) { Minimalny wiek to {{ w.errors?.['min'].min }} lat. }
      @if (w.errors?.['max']) { Maksymalny wiek to {{ w.errors?.['max'].max }} lat. }
    </div>
  </div>

  <div class="mb-3">
    <label class="form-label" for="email">E-mail *</label>
    <input id="email" name="email" type="email" class="form-control"
           required email [(ngModel)]="email" #e="ngModel"
           [class.is-invalid]="e.invalid && e.touched">
    <div class="invalid-feedback">
      @if (e.errors?.['required']) { Podaj adres e-mail. }
      @if (e.errors?.['email']) { To nie jest poprawny adres e-mail. }
    </div>
  </div>
</div>
wynik
wiek 12  → Minimalny wiek to 13 lat.
wiek 25  → Maksymalny wiek to 19 lat.
e-mail „ala” → To nie jest poprawny adres e-mail.

Komunikaty są w invalid-feedback bez @if dookoła — Bootstrap sam je chowa, gdy pole nie ma is-invalid. Dlatego każde pole razem ze swoim komunikatem siedzi we własnym <div class="mb-3">: Bootstrap pokazuje każdy invalid-feedback stojący za błędnym polem w tym samym elemencie. Bez osobnych div-ów błąd wieku pokazałby też komunikat pod e-mailem.

email przepuści ala@szkola

Walidator email uzna za poprawny adres bez kropki w domenie — jest zgodny ze standardem, choć w praktyce nie istnieje. Jeśli wymagasz domeny z kropką, dołóż pattern.

Zatrzymaj się

Atrybuty min i max pojawiły się już w lekcji 19 i wtedy nie chroniły przed wpisaniem złej liczby. Co zmieniło się teraz?

PRZYKŁAD 4

Cały formularz: blokada zapisu

Trzy pola z walidacją w jednym formularzu. Po kliknięciu „Zapisz” sprawdzamy f.invalid. Jeśli są błędy — oznaczamy wszystkie pola jako dotknięte (markAllAsTouched), żeby od razu było widać, co poprawić. Jeśli nie — zapisujemy. Typ NgForm importujesz z @angular/forms.

src/app/nowa-26-4/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';

@Component({
    selector: 'app-demo',
    imports: [FormsModule],
    templateUrl: './demo.html'
})
export class Demo {
    readonly imie = signal('');
    readonly telefon = signal('');
    readonly regulamin = signal(false);
    readonly wynik = signal('');

    zapisz(f: NgForm): void {
        if (f.invalid) {
            f.form.markAllAsTouched();
            this.wynik.set('Popraw zaznaczone pola.');
            return;
        }
        this.wynik.set('Zapisano: ' + this.imie() + ', tel. ' + this.telefon());
    }
}
src/app/nowa-26-4/demo.html — pełny plik
<div class="p-3" style="width: 26rem">
  <form #f="ngForm" (ngSubmit)="zapisz(f)">
    <div class="mb-3">
      <label class="form-label" for="imie">Imię i nazwisko *</label>
      <input id="imie" name="imie" class="form-control" required minlength="5"
             [(ngModel)]="imie" #i="ngModel" [class.is-invalid]="i.invalid && i.touched">
      <div class="invalid-feedback">Podaj imię i nazwisko (co najmniej 5 znaków).</div>
    </div>

    <div class="mb-3">
      <label class="form-label" for="tel">Telefon rodzica *</label>
      <input id="tel" name="telefon" class="form-control" required pattern="[0-9]{9}"
             placeholder="9 cyfr bez spacji"
             [(ngModel)]="telefon" #t="ngModel" [class.is-invalid]="t.invalid && t.touched">
      <div class="invalid-feedback">Numer telefonu to 9 cyfr.</div>
    </div>

    <div class="form-check mb-3">
      <input id="reg" name="regulamin" class="form-check-input" type="checkbox" required
             [(ngModel)]="regulamin" #r="ngModel" [class.is-invalid]="r.invalid && r.touched">
      <label class="form-check-label" for="reg">Akceptuję regulamin *</label>
      <div class="invalid-feedback">Akceptacja regulaminu jest wymagana.</div>
    </div>

    <button class="btn btn-primary" type="submit">Zapisz</button>
    <p class="mt-3">{{ wynik() }}</p>
    <p class="small text-secondary">Formularz poprawny: {{ f.valid }}</p>
  </form>
</div>
wynik
„Zapisz” przy pustym formularzu: trzy czerwone pola, „Popraw zaznaczone pola.”
po poprawnym wypełnieniu:        Zapisano: Jan Nowak, tel. 600100200

Na checkboxie required oznacza „musi być zaznaczony”. Ostatni wiersz pokazuje f.valid — zmienia się na true dopiero przy komplecie.

Zatrzymaj się

Przycisk „Zapisz” nie jest tu blokowany przez [disabled]="f.invalid". Jaka jest przewaga tego rozwiązania dla użytkownika, który nie wie, czego brakuje?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
required, minlength, pattern, min, max, emailwalidatoryAtrybuty HTML.
#x="ngModel"uchwyt do kontrolkiInna nazwa niż sygnał.
x.invalid, x.touched, x.dirtystan polaBłąd: invalid && touched.
x.errors?.['klucz']konkretny błądBezpieczny dostęp ?..
is-invalid, is-validczerwona / zielona ramkaBootstrap.
invalid-feedbackkomunikat pod polemPole i komunikat w jednym div.
f.invalid, f.validstan całego formularzaZ #f="ngForm".
f.form.markAllAsTouched()pokaż wszystkie błędyPo próbie zapisu.
NgFormtyp formularzaImport z @angular/forms.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
#imie="ngModel" i sygnał imieKonflikt nazw: w szablonie imie oznacza zmienną szablonową, a nie sygnał. Nadaj zmiennej inną nazwę.
l.errors['required'] bez ?.Błąd kompilacji — errors może być null. Pisz l.errors?.['required'].
invalid-feedback nie pojawia się mimo błęduKomunikat musi stać za polem z is-invalid i w tym samym elemencie nadrzędnym.
Kilka pól z komunikatami w jednym divBłąd w pierwszym polu pokazuje komunikaty wszystkich pól poniżej. Każde pole — własny div.
Komunikaty widoczne od samego początkuBrak warunku touched.
Liczenie na to, że required zablokuje wysłanie(ngSubmit) się wywoła — sprawdź f.invalid.
pattern="[0-9]{9}", a użytkownik wpisuje „600 100 200”Błąd wzorca przez spacje. Napisz w placeholder, w jakiej postaci podać dane, albo usuń spacje w (ngModelChange) (lekcja 25).
ZADANIA

Zadania

ZAD 1Nick obowiązkowy★☆☆

Pole „Nick” z walidatorami required i minlength="3". Dwa różne komunikaty i zielona ramka, gdy pole jest poprawne.

Kryterium sukcesu: komunikaty pojawiają się dopiero po opuszczeniu pola i zawsze pasują do błędu.

Podpowiedź

Przykład 2 z mniejszą liczbą walidatorów.

ZAD 2Numer legitymacji★☆☆

Pole „Numer legitymacji” — dokładnie 6 cyfr. Komunikat ma mówić, ile cyfr wpisano, gdy jest ich za mało, i że dozwolone są tylko cyfry, gdy pojawi się litera.

Kryterium sukcesu: dla „123” widać „wpisano 3 z 6 cyfr”, dla „12a456” — komunikat o cyfrach, dla „123456” — zielona ramka.

Podpowiedź

required minlength="6" maxlength="6" pattern="[0-9]*" i odczyt errors?.['minlength'].actualLength.

ZAD 3Rejestracja konta★★☆

Formularz: login (4–16 znaków, małe litery i cyfry), e-mail, hasło (co najmniej 8 znaków) i powtórzenie hasła. Przycisk „Załóż konto” działa jak w przykładzie 4.

Kryterium sukcesu: każde pole ma własne komunikaty; przy różnych hasłach pod drugim polem pojawia się „Hasła się różnią”, a zapis się nie udaje.

Podpowiedź

Zgodności haseł nie sprawdzi żaden atrybut. Policz ją w computed z dwóch sygnałów i dołóż do warunku w metodzie: if (f.invalid || !haslaZgodne()).

ZAD 4Licznik błędów★★☆

Rozbuduj przykład 4: nad przyciskiem pokaż „Do poprawienia: X pól”, liczone na podstawie stanu kontrolek, a przycisk niech ma napis „Zapisz (3 błędy)” albo „Zapisz”, gdy wszystko jest dobrze.

Kryterium sukcesu: liczba zmienia się przy poprawianiu kolejnych pól i spada do zera przy poprawnym formularzu.

Podpowiedź

W szablonie: (i.invalid ? 1 : 0) + (t.invalid ? 1 : 0) + (r.invalid ? 1 : 0) — albo metoda w klasie, która dostaje f i liczy Object.keys(f.controls) z błędami.

ZAD 5Rezerwacja pracowni★★★

Formularz rezerwacji pracowni: nazwisko nauczyciela (obowiązkowe), sala (select z opcją „— wybierz —”, obowiązkowy), data (obowiązkowa), liczba uczniów (1–16) i cel zajęć (textarea, 10–200 znaków). Udane rezerwacje trafiają na listę pod formularzem.

Kryterium sukcesu: niepoprawnego formularza nie da się zapisać, wszystkie błędy widać po kliknięciu przycisku, a poprawna rezerwacja pojawia się na liście i czyści formularz.

Podpowiedź

required na <select> działa z opcją [ngValue]="null". Po zapisie ustaw sygnały na wartości początkowe i wywołaj f.form.markAsUntouched(), żeby czerwone ramki nie pojawiły się od razu.

ZAD 6Zgłoszenie na wycieczkę★★★

Formularz: imię i nazwisko (min. 5 znaków), PESEL (11 cyfr), telefon rodzica (9 cyfr, spacje usuwane w locie), wiek (13–19), rozmiar koszulki (radio, obowiązkowy) i zgoda rodzica (checkbox, wymagany). Po wysłaniu poprawnego formularza pokaż podsumowanie w karcie i przycisk „Wypełnij ponownie”; przy błędach — ramkę alert-danger z liczbą błędnych pól.

Kryterium sukcesu: wszystkie walidatory działają, spacje w telefonie znikają podczas pisania, a podsumowanie pojawia się tylko dla poprawnych danych.

Podpowiedź

Telefon w wersji rozpisanej z lekcji 25 plus pattern="[0-9]{9}". Radio z required: atrybut wystarczy na jednym przycisku grupy.

PODSUMOWANIE

Co trzeba zapamiętać

  • Walidatory włączasz atrybutami: required, minlength, maxlength, pattern, min, max, email.
  • #x="ngModel" daje dostęp do stanu pola: invalid, touched, dirty, errors.
  • Błąd pokazujesz przy invalid && touched, klasami is-invalid i invalid-feedback.
  • Komunikat dobierasz po kluczu w errors, z bezpiecznym dostępem ?..
  • (ngSubmit) wywołuje się także dla błędnego formularza: sprawdź f.invalid i w razie potrzeby markAllAsTouched().

Sprawdź się: użytkownik kliknął „Zapisz”, nic się nie stało i nie wie dlaczego. Jakie dwie rzeczy dodasz, żeby wiedział, co poprawić?

Dokumentacja: angular.dev — walidacja formularzy.

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