Zdarzenia ( ) i zmienne szablonowe #
Do tej pory dane płynęły w jedną stronę: z klasy na ekran. Teraz odwrócisz kierunek — kliknięcie i pisanie w polu uruchomią metodę w klasie. Poznasz też zmienną szablonową, która daje szablonowi dostęp do konkretnego pola formularza.
Czego się dziś nauczysz
- Obsłużysz kliknięcie zapisem
(click)="metoda()"i zmienisz pole klasy w metodzie. - Przekażesz metodzie własny argument, żeby jedna metoda obsłużyła kilka przycisków.
- Odczytasz wpisany tekst z obiektu
$eventw zdarzeniu(input). - Odróżnisz
(input)od(change). - Nadasz elementowi nazwę
#polei użyjesz jej, żeby odczytać wartość, wyczyścić pole i ustawić w nim kursor. - Zareagujesz tylko na klawisz Enter zapisem
(keyup.enter). - Rozpoznasz ostrzeżenie NG8111 przy metodzie bez nawiasów.
Przygotowanie: lekcje 05–07 — umiesz pokazać dane i przekazać wartość do właściwości. Przewidywany czas: 45 min z zadaniami.
Ekran odpowiada użytkownikowi
Wiązanie [ ] prowadzi dane z klasy do szablonu. Zdarzenia prowadzą je w przeciwną stronę: użytkownik coś robi, a klasa się o tym dowiaduje. Zapamiętaj tę symetrię: nawias kwadratowy to wejście do elementu, nawias okrągły to wyjście z niego. Dwukierunkowe [(ngModel)] z lekcji 17 to po prostu oba naraz.
<button (click)="zapisz()">Zapisz</button>
<!-- ↑ zdarzenie ↑ co zrobić — wywołanie metody z klasy -->
W nawiasach okrągłych stoi nazwa zdarzenia bez przedrostka on (w czystym HTML-u byłoby onclick). W cudzysłowie — zwykle wywołanie metody, z nawiasami. Po każdym obsłużonym zdarzeniu Angular sam odświeża szablon, więc nowa wartość pola od razu pojawia się na ekranie.
| Zdarzenie | Kiedy zachodzi |
|---|---|
(click) | kliknięcie myszą; na przycisku także naciśnięcie Enter lub spacji |
(input) | po każdej zmianie zawartości pola, także przy wklejeniu |
(change) | dopiero po wyjściu z pola po zmianie (przy polach tekstowych) |
(keyup.enter) | puszczenie klawisza Enter — Angular sam sprawdza, który to klawisz |
(blur), (focus) | wyjście z pola, wejście do pola |
Decyzje trzymamy w klasie
W obsłudze zdarzenia wolno napisać krótką instrukcję, a kilka oddzielić średnikiem. Ale wszystko, co ma warunek albo więcej niż jedną czynność na danych, przenieś do metody. Szablon ma pokazywać, nie decydować.
Skąd wziąć wpisany tekst: $event i #zmienna
$event
Nazwa $event oznacza obiekt opisujący zdarzenie. Przy kliknięciu rzadko jest potrzebny, przy polu tekstowym — prawie zawsze, bo przez $event.target dochodzisz do pola i jego wartości.
Zmienna szablonowa #nazwa
Zapis #pole przy elemencie HTML nadaje mu nazwę, przez którą sięgasz do niego w tym samym szablonie: do wartości (pole.value), do metody ustawiającej kursor (pole.focus()). To najprostsza droga, gdy potrzebujesz wartości jednego pola w chwili kliknięcia — bez budowania całego formularza.
<input #pole class="form-control">
<button (click)="pokaz(pole.value)">Pokaż</button>
<!-- pole.value — tekst wpisany w polu w chwili kliknięcia -->
Zmienna szablonowa żyje tylko w szablonie — w klasie jej nie ma. Do klasy trafia tylko to, co przekażesz jako argument metody.
Pierwsze kliknięcie
Nawias okrągły przy nazwie zdarzenia, a w cudzysłowie — co ma się wtedy stać. Pole wiadomosc nie jest readonly, bo metoda je zmienia.
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
wiadomosc = 'Jeszcze nic nie kliknięto.';
zapisz(): void {
this.wiadomosc = 'Dziękujemy za zapis!';
}
}
<div class="p-3">
<button class="btn btn-primary" (click)="zapisz()">Zapisz się</button>
<p class="mt-3">{{ wiadomosc }}</p>
</div>
przed kliknięciem: Jeszcze nic nie kliknięto.
po kliknięciu: Dziękujemy za zapis!
Metoda nazywa się zapisz, a w szablonie piszemy zapisz() — z nawiasami, bo chcemy ją wywołać.
Zatrzymaj się
Kliknij drugi raz. Dlaczego nic się już nie zmienia, chociaż metoda wykonała się ponownie? Potem usuń nawiasy ((click)="zapisz") i sprawdź terminal — jaki kod ma ostrzeżenie i co się dzieje po kliknięciu?
Metoda z argumentem
Jedna metoda obsługuje trzy przyciski, bo w argumencie dostaje informację, który to był.
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
wybrane = '';
wybierz(nazwa: string): void {
this.wybrane = nazwa;
}
}
<div class="p-3">
<button class="btn btn-outline-primary me-2" (click)="wybierz('Turniej retro')">Turniej retro</button>
<button class="btn btn-outline-primary me-2" (click)="wybierz('Noc robotów')">Noc robotów</button>
<button class="btn btn-outline-primary" (click)="wybierz('Druk 3D')">Druk 3D</button>
<p class="mt-3">Wybrano: {{ wybrane }}</p>
</div>
Wybrano: Noc robotów
Tekst w apostrofach to zwykły argument — taki sam, jaki przekazałbyś w TypeScripcie. Apostrofy, bo cały atrybut stoi już w cudzysłowie.
Zatrzymaj się
Gdyby przycisków było dwadzieścia, ten szablon miałby dwadzieścia niemal identycznych wierszy. Rozwiązanie czeka w lekcji 13, przy @for.
$event — tekst wpisywany na bieżąco
Zdarzenie (input) zachodzi po każdej zmianie zawartości pola. Metoda dostaje obiekt zdarzenia i wyciąga z niego wpisany tekst.
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
tekst = '';
znakow = 0;
przepisz(zdarzenie: Event): void {
const pole = zdarzenie.target as HTMLInputElement;
this.tekst = pole.value;
this.znakow = pole.value.length;
}
}
<div class="p-3">
<input class="form-control" style="width: 22rem"
placeholder="Nazwa wydarzenia" (input)="przepisz($event)">
<p class="mt-2">Wpisano: {{ tekst }} ({{ znakow }} znaków)</p>
</div>
Wpisano: Turniej (7 znaków)
zdarzenie.target to element, który wywołał zdarzenie. TypeScript nie wie, że to pole tekstowe, więc mówimy mu to zapisem as HTMLInputElement — dopiero wtedy wolno odczytać .value. Klasa form-control to pole tekstowe w stylu Bootstrapa.
Zatrzymaj się
Zmień (input) na (change) i wpisz kilka liter. Kiedy teraz zmienia się napis pod polem? Wróć do (input).
Zmienna szablonowa i klawisz Enter
Pole ma nazwę #pole. Przycisk i klawisz Enter przekazują jego wartość do metody, a potem czyszczą pole. Drugi przycisk ustawia kursor w polu, nie dotykając klasy.
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
lista: string[] = [];
komunikat = '';
dodaj(nazwa: string): void {
const czysta = nazwa.trim();
if (czysta.length === 0) {
this.komunikat = 'Wpisz nazwę wydarzenia.';
return;
}
this.lista = [...this.lista, czysta];
this.komunikat = 'Dodano: ' + czysta;
}
}
<div class="p-3" style="width: 26rem">
<input #pole class="form-control mb-2" placeholder="Nazwa wydarzenia"
(keyup.enter)="dodaj(pole.value); pole.value = ''">
<button class="btn btn-primary me-2" (click)="dodaj(pole.value); pole.value = ''; pole.focus()">Dodaj</button>
<button class="btn btn-outline-secondary" (click)="pole.focus()">Kursor do pola</button>
<p class="mt-3 mb-1">{{ komunikat }}</p>
<p class="text-muted">Pozycji: {{ lista.length }} — {{ lista.join(', ') }}</p>
</div>
Wpisz nazwę wydarzenia.
Pozycji: 2 — Turniej retro, Noc robotów
W obsłudze zdarzenia stoją trzy instrukcje oddzielone średnikiem: wywołanie metody, wyczyszczenie pola i ustawienie kursora. Decyzja (czy nazwa nie jest pusta) jest w klasie. trim() obcina spacje z początku i końca, więc same spacje też liczą się jako pusta nazwa. Nową pozycję dokładamy, tworząc nową tablicę [...this.lista, czysta] — dlaczego tak, wyjaśni się w lekcji 10.
Zatrzymaj się
lista.join(', ') skleja elementy tablicy w jeden tekst. To prowizorka — porządną listę, z każdą pozycją w osobnym wierszu, zrobisz w lekcji 13 blokiem @for.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
(click)="metoda()" | obsługa kliknięcia | Bez nawiasów — ostrzeżenie NG8111. |
(click)="metoda('tekst')" | wywołanie z argumentem | Tekst w apostrofach. |
(input)="metoda($event)" | reakcja na każdą zmianę pola | $event trzeba przekazać jawnie. |
(change) | reakcja po wyjściu z pola | Przy polu tekstowym. |
(keyup.enter) | tylko klawisz Enter | Także (keyup.escape). |
zdarzenie.target as HTMLInputElement | element, który wywołał zdarzenie | Potem .value. |
#pole | nazwa elementu w szablonie | Tylko w tym szablonie. |
pole.value | tekst w polu | Przypisanie pole.value = '' czyści pole. |
pole.focus() | kursor do pola | Metoda elementu HTML. |
tekst.trim() | tekst bez spacji na brzegach | Do sprawdzania pustych wpisów. |
[...lista, x] | nowa tablica z dodanym elementem | Szczegóły w lekcji 10. |
lista.join(', ') | tablica sklejona w tekst | Prowizorka do lekcji 13. |
form-control | pole tekstowe Bootstrapa | Na całą szerokość rodzica. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
(click)="zapisz" bez nawiasów | Kliknięcie nic nie robi, w terminalu ostrzeżenie NG8111. Bez nawiasów podajesz funkcję, ale nikt jej nie wywołuje. |
(onclick)="zapisz()" | Kompiluje się bez błędu, ale kliknięcie nic nie robi — zdarzenia o nazwie onclick nie ma. W szablonach nazwy są bez przedrostka on. |
(input)="przepisz()" bez $event | Błąd kompilacji: metoda oczekuje argumentu. Obiekt zdarzenia trzeba przekazać jawnie. |
zdarzenie.target.value bez as HTMLInputElement | Błąd TypeScriptu: target może być czymkolwiek, więc nie ma pewności, że ma value. |
(change) zamiast (input) przy pisaniu na bieżąco | Reakcja dopiero po wyjściu z pola. |
this.pole.value w metodzie klasy | TS2339 — zmienna szablonowa nie istnieje w klasie. Przekaż pole.value jako argument. |
(click)="licznik = licznik + 1; if (…)" | Instrukcji if nie wolno w szablonie. Warunki przenieś do metody. |
Zadania
Zrób dwa przyciski: zielony „Potwierdź” ustawia napis „zapisano”, szary „Anuluj” — „anulowano”. Użyj jednej metody z argumentem.
Kryterium sukcesu: oba przyciski zmieniają ten sam napis na ekranie, a w klasie jest jedna metoda.
Podpowiedź
Wzoruj się na przykładzie 2.
Zrób przycisk „👍 Lubię to” i przycisk „Wyzeruj”. Pod spodem napis „Polubień: 0”, który rośnie o 1 po każdym kliknięciu pierwszego przycisku i wraca do zera po kliknięciu drugiego.
Kryterium sukcesu: licznik rośnie po każdym kliknięciu i wraca do 0 po wyzerowaniu.
Podpowiedź
Pole polubienia = 0 i dwie metody: polub() z this.polubienia = this.polubienia + 1 oraz wyzeruj().
Zrób pole na krótki opis wydarzenia, pod którym jest napis „Znaków: 12 / 40”. Gdy tekst przekroczy 40 znaków, pole ma dostać czerwoną ramkę, a pod nim ma się pojawić komunikat „Za długi opis”.
Kryterium sukcesu: ramka i komunikat pojawiają się dokładnie od 41. znaku i znikają po skróceniu tekstu.
Podpowiedź
(input) i $event jak w przykładzie 3. Ramka: [class.is-invalid]="znakow > 40" (klasa Bootstrapa dla błędnego pola), komunikat: [hidden] z lekcji 06.
Zrób kartę biletu za 12.50 zł z przyciskami „−” i „+” zmieniającymi liczbę biletów od 0 do 10. Pokaż liczbę biletów i kwotę do zapłaty z dwoma miejscami po kropce. Przycisk „−” ma być zablokowany przy 0, a „+” przy 10.
Kryterium sukcesu: liczba nie spada poniżej 0 ani nie rośnie ponad 10, kwota zgadza się z rachunkiem, a przyciski blokują się na granicach.
Podpowiedź
Granice sprawdź w metodach dodaj() i odejmij() instrukcją if. Blokada: [disabled]="ilosc === 0".
Rozbuduj przykład 4 o trzy rzeczy: (a) komunikat o pustej nazwie ma być czerwony, a o dodaniu — zielony; (b) nie da się dodać nazwy, która już jest na liście (komunikat „Takie wydarzenie już jest”); (c) przycisk „Wyczyść listę” usuwa wszystkie pozycje i ustawia kursor w polu.
Kryterium sukcesu: duplikat i pusta nazwa dają czerwone komunikaty, poprawna nazwa — zielony, a po wyczyszczeniu licznik pokazuje 0 i kursor jest w polu.
Podpowiedź
Duplikat sprawdzisz w metodzie: this.lista.includes(czysta). Kolor: pole blad (true/false) ustawiane w metodzie i [class.text-danger]/[class.text-success]. Czyszczenie: this.lista = [].
Zrób pole, w które uczeń wpisuje liczbę punktów z testu (0–50). Na bieżąco, przy każdej zmianie, pokaż ocenę według progów: 0–19 → 1, 20–29 → 2, 30–37 → 3, 38–44 → 4, 45–48 → 5, 49–50 → 6. Gdy wpisano coś, co nie jest liczbą z zakresu 0–50, zamiast oceny pokaż „Podaj liczbę od 0 do 50”. Ocena 1 ma być czerwona, a 5 i 6 — zielone.
Kryterium sukcesu: dla 19, 20, 44, 49 i 51 oraz dla tekstu „abc” widać właściwe oceny albo komunikat.
Podpowiedź
Tekst z pola zamień na liczbę: Number(pole.value); nie-liczba daje NaN, co sprawdzisz przez Number.isNaN(x). Progi — kolejne if w metodzie ocena() albo w metodzie obsługującej (input).
Co trzeba zapamiętać
(zdarzenie)="metoda()"— nawiasy okrągłe przy zdarzeniu i wywołanie metody z nawiasami.- Po obsłużonym zdarzeniu Angular sam odświeża ekran.
- Metodzie można przekazać własny argument albo
$event. (input)reaguje na każdą literę,(change)dopiero po wyjściu z pola,(keyup.enter)— na Enter.#poledaje dostęp do elementu w szablonie:pole.value,pole.focus().- Zmienna szablonowa nie istnieje w klasie — wartość przekazujesz argumentem.
- Warunki i obliczenia należą do klasy, nie do szablonu.
Sprawdź się: masz pole tekstowe i chcesz reagować na każdą literę. Które zdarzenie wybierzesz i co musisz przekazać metodzie? A jeśli wystarczy Ci wartość w chwili kliknięcia przycisku obok?
Dokumentacja: angular.dev — obsługa zdarzeń.