Czym jest sygnał
Zrobisz stoper, który liczy sekundy w pamięci, a na ekranie stoi na zerze. To nie jest błąd, tylko najważniejsza lekcja tego kursu: sygnał odpowiada na pytanie, skąd Angular ma wiedzieć, że coś trzeba przerysować.
Czego się dziś nauczysz
- Wyjaśnisz, kiedy zmiana zwykłego pola klasy nie odświeża ekranu.
- Uruchomisz kod co sekundę funkcją
setIntervali zatrzymasz go przezclearInterval. - Utworzysz sygnał funkcją
signal(wartośćPoczątkowa). - Odczytasz wartość sygnału z nawiasami — w szablonie i w klasie.
- Ustawisz nową wartość metodą
set. - Rozpoznasz napis
[Signal: …]i ostrzeżenia NG8109/NG8117 przy odczycie bez nawiasów. - Umieścisz w sygnale tekst, wartość logiczną i obiekt.
Przygotowanie: lekcje 05–08 — umiesz pokazać dane w szablonie i obsłużyć kliknięcie. Przewidywany czas: 2 × 45 min z zadaniami.
Skąd Angular wie, co przerysować
W lekcji 08 zmieniałeś zwykłe pola klasy i ekran od razu się odświeżał. To zasługa zdarzenia: po każdym (click), (input) czy (keyup.enter) z szablonu Angular przerysowuje komponent — na wszelki wypadek.
Nie każda zmiana zaczyna się jednak od kliknięcia. Stoper tyka co sekundę. Odpowiedź z serwera przychodzi po chwili. Wtedy nikt nie mówi Angularowi: „ta liczba się zmieniła, odśwież ją”. Wartość leży w pamięci, a ekran pokazuje stary stan.
Angular rozwiązuje to sygnałami. Sygnał to wartość, która przy każdej zmianie sama zgłasza: „byłem użyty w tym miejscu szablonu, odśwież je”. Dlatego od tej lekcji każdą daną, która się zmienia i jest pokazywana na ekranie, trzymamy w sygnale — niezależnie od tego, czy zmienia ją kliknięcie, stoper czy serwer.
Kartka i tablica ogłoszeń
Zwykłe pole to kartka z liczbą — ktoś musi sam zauważyć, że się zmieniła. Sygnał to tablica ogłoszeń — każdy, kto raz na nią spojrzał, dostaje powiadomienie o zmianie.
Dlaczego w Angularze 22 to tak ważne
Angular 22 działa domyślnie w trybie zoneless i odświeża komponent tylko wtedy, gdy ma ku temu powód: zdarzenie z szablonu albo zmianę sygnału. Starsze wersje (i materiały w internecie) odświeżały „wszystko po wszystkim”, także po setInterval. Jeśli w cudzym kodzie zwykłe pole zmieniane w stoperze działało, to tylko dlatego.
Sygnał w trzech krokach
| Krok | Zapis | Co znaczy |
|---|---|---|
| Tworzenie | readonly licznik = signal(0); | sygnał z wartością początkową 0; signal importujesz z @angular/core |
| Odczyt | licznik() w szabloniethis.licznik() w klasie | „daj mi wartość” — zawsze z nawiasami |
| Zmiana | this.licznik.set(5); | nowa wartość i odświeżenie każdego miejsca, które ją odczytało |
readonly przy sygnale oznacza, że nie podmienisz samego sygnału na inny. Wartość w środku zmieniasz do woli przez set.
setInterval i clearInterval
Do stopera potrzebujesz funkcji, która wykonuje kod co jakiś czas. setInterval(() => { … }, 1000) uruchamia kod w klamrach co 1000 milisekund i zwraca numer stopera. Ten numer przekazany do clearInterval(numer) zatrzymuje stoper. Zapis () => { … } to funkcja strzałkowa znana z TypeScriptu.
Stoper na zwykłym polu — i zaskoczenie
Liczymy sekundy w zwykłym polu. Drugi przycisk, Pokaż, wywołuje pustą metodę: niczego nie zmienia, tylko jest kliknięciem.
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
sekundy = 0;
dziala = false;
start(): void {
if (this.dziala) {
return; // drugi stoper nie jest potrzebny
}
this.dziala = true;
setInterval(() => {
this.sekundy = this.sekundy + 1;
console.log('w klasie sekundy =', this.sekundy);
}, 1000);
}
pokaz(): void {
// celowo pusta: samo kliknięcie wystarczy, żeby Angular przerysował ekran
}
}
<div class="p-3">
<button class="btn btn-primary me-2" (click)="start()">Start</button>
<button class="btn btn-outline-secondary" (click)="pokaz()">Pokaż</button>
<p class="mt-3 fs-4">Stoper: {{ sekundy }} s</p>
</div>
po kliknięciu Start i 3 sekundach: Stoper: 0 s (konsola F12: 1, 2, 3)
po kliknięciu Pokaż: Stoper: 3 s i znowu stoi
Otwórz konsolę przeglądarki (F12) i kliknij Start. W konsoli co sekundę pojawia się kolejna liczba — pole naprawdę rośnie. Na ekranie nadal jest 0 s. Kliknij Pokaż: liczba skacze do aktualnej wartości i znowu staje. fs-4 to klasa Bootstrapa powiększająca tekst.
Zatrzymaj się
Metoda pokaz jest pusta, a jednak ekran się zmienił. Co w takim razie odświeżyło ekran — kod metody czy samo kliknięcie? I dlaczego tykanie stopera tego nie robi?
Ten sam stoper na sygnale
Zmieniamy dokładnie trzy rzeczy: sposób zapisania pola, sposób zmiany wartości i odczyt w szablonie. Przycisk Pokaż nie jest już potrzebny.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly sekundy = signal(0);
dziala = false;
start(): void {
if (this.dziala) {
return;
}
this.dziala = true;
setInterval(() => {
this.sekundy.set(this.sekundy() + 1);
}, 1000);
}
}
<div class="p-3">
<button class="btn btn-primary" (click)="start()">Start</button>
<p class="mt-3 fs-4">Stoper: {{ sekundy() }} s</p>
</div>
Stoper: 1 s … 2 s … 3 s … (co sekundę, bez klikania)
Porównaj oba pliki demo.ts — różnią się trzema miejscami: signal(0), .set(this.sekundy() + 1) i nawiasy przy odczycie w szablonie. W imporcie doszło słowo signal.
Zatrzymaj się
Pole dziala zostało zwykłym polem. Czy to błąd? Sprawdź, czy ta wartość jest gdziekolwiek pokazywana na ekranie.
Odczyt sygnału — z nawiasami i bez
Najczęstszy błąd początkującego. Zobaczmy, co się dzieje, gdy nawiasów zabraknie.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly punkty = signal(42);
readonly nazwa = signal('Turniej retro');
}
<div class="p-3">
<ul class="list-group" style="width: 26rem">
<li class="list-group-item">Poprawnie: {{ punkty() }}</li>
<li class="list-group-item">Bez nawiasów: {{ punkty }}</li>
<li class="list-group-item">Tekst: {{ nazwa() }}</li>
<li class="list-group-item">Obliczenie: {{ punkty() * 2 }}</li>
</ul>
</div>
Poprawnie: 42
Bez nawiasów: [Signal (punkty): 42]
Tekst: Turniej retro
Obliczenie: 84
▲ [WARNING] NG8109: punkty is a function and should be invoked: punkty()
▲ [WARNING] NG8117: Function in text interpolation should be invoked: punkty()
Drugi wiersz pokazuje opis samego sygnału zamiast wartości. Strona działa dalej, bo to tylko ostrzeżenia — a Angular podpowiada nawet, co dopisać.
Zatrzymaj się
Sygnał to pudełko z wartością. Nazwa sygnału to pudełko, a nawiasy oznaczają „otwórz i wyjmij”. Jak zapytasz o pudełko, dostaniesz pudełko.
Sygnał nie musi trzymać liczby
W sygnale może być tekst, wartość logiczna albo cały obiekt. Zasada odczytu jest zawsze ta sama.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly nazwa = signal('Turniej retro');
readonly zapisany = signal(false);
readonly wydarzenie = signal({ tytul: 'Noc robotów', sala: 12 });
zapisz(): void {
this.zapisany.set(true);
}
zmienNazwe(): void {
this.nazwa.set('Warsztat druku 3D');
}
}
<div class="card m-3 p-3" style="width: 24rem">
<h5>{{ nazwa() }}</h5>
<p>Zapisany: {{ zapisany() }}</p>
<p>{{ wydarzenie().tytul }}, sala {{ wydarzenie().sala }}</p>
<button class="btn btn-primary" (click)="zapisz()">Zapisz się</button>
<button class="btn btn-outline-secondary mt-2" (click)="zmienNazwe()">Zmień nazwę</button>
</div>
Warsztat druku 3D
Zapisany: true
Noc robotów, sala 12
Zwróć uwagę na wydarzenie().tytul — najpierw wyjmujesz obiekt z sygnału (nawiasy), dopiero potem sięgasz po jego pole (kropka).
Zatrzymaj się
Napisz w szablonie wydarzenie.tytul bez nawiasów. Czy kompilator to wyłapie? Przeczytaj komunikat.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
import { signal } from '@angular/core' | import funkcji | Obok Component. |
signal(wartość) | nowy sygnał | Wartość początkowa obowiązkowa. |
sygnal() | odczyt wartości | W klasie: this.sygnal(). |
sygnal.set(x) | ustawienie nowej wartości | Odświeża miejsca, które odczytały sygnał. |
obiekt().pole | pole obiektu z sygnału | Najpierw nawiasy, potem kropka. |
setInterval(funkcja, ms) | kod uruchamiany co ms milisekund | Zwraca numer stopera. |
clearInterval(numer) | zatrzymanie stopera | Numer z setInterval. |
console.log(…) | wypisanie w konsoli przeglądarki | F12 → Console. |
fs-4, fs-5 | większy tekst | Bootstrap; mniejsza liczba = większy tekst. |
| NG8109, NG8117 | sygnał/metoda bez nawiasów | Ostrzeżenia, nie błędy. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
{{ licznik }} zamiast {{ licznik() }} | Napis [Signal (licznik): 0] zamiast liczby i ostrzeżenie NG8109. Zaglądaj do terminala, nie tylko do przeglądarki. |
this.licznik = 5 | Błąd kompilacji — pole jest readonly. Wartość ustawia się przez .set(5). |
this.licznik.set(licznik() + 1) | Błąd: w klasie przed nazwą sygnału potrzebne jest this — także w środku nawiasów. |
this.licznik + 1 | Błąd typu: dodajesz liczbę do sygnału, a nie do jego wartości. Potrzebne this.licznik() + 1. |
Zwykłe pole zmieniane w setInterval albo setTimeout | Wartość zmienia się w pamięci, ekran zostaje stary — do najbliższego kliknięcia. To przykład 1. |
| „Przecież po kliknięciu zwykłe pole działa” | Działa, bo kliknięcie odświeża ekran. Gdy dojdzie stoper, serwer albo computed z lekcji 11, przestanie. Dane, które się zmieniają, trzymaj w sygnałach. |
signal() bez wartości | Błąd: sygnał zawsze zaczyna od jakiejś wartości — signal(0), signal(''), signal(false). |
Brak signal w imporcie | Cannot find name 'signal’. Dopisz go w import { Component, signal } …. |
Zadania
Zrób licznik na sygnale z trzema przyciskami: „+1”, „−1” i „Zeruj”.
Kryterium sukcesu: wszystkie trzy przyciski działają, a liczba na ekranie zmienia się natychmiast.
Podpowiedź
Zwiększanie: this.licznik.set(this.licznik() + 1). Odejmowanie różni się jednym znakiem.
Zrób sygnał logiczny ciszaWlaczona i przycisk, który go przełącza. Pod przyciskiem pokaż stan słowami: „włączona” albo „wyłączona”, a sam przycisk niech zmienia kolor (szary, gdy cisza wyłączona; ciemny, gdy włączona).
Kryterium sukcesu: każde kliknięcie zmienia napis i kolor przycisku.
Podpowiedź
Przełączenie to ustawienie wartości przeciwnej: set(!this.ciszaWlaczona()). Kolor: [class.btn-dark] i [class.btn-outline-secondary] z lekcji 06.
Zrób sygnał z nazwą wydarzenia i trzy przyciski z nazwami: „Turniej retro”, „Noc robotów”, „Druk 3D”. Kliknięcie ustawia nazwę w sygnale. Pod przyciskami pokaż nazwę wielkimi literami i jej długość.
Kryterium sukcesu: po każdym kliknięciu zmienia się nazwa, wielkie litery i liczba znaków.
Podpowiedź
Jedna metoda z argumentem (lekcja 08). W szablonie: {{ nazwa().toUpperCase() }}, {{ nazwa().length }}.
Pokaż aktualną godzinę w formacie 14:05:09, która zmienia się co sekundę bez żadnego klikania. Zegar ma ruszyć sam, od razu po otwarciu strony.
Kryterium sukcesu: godzina na ekranie zgadza się z zegarem komputera i odświeża się co sekundę.
Podpowiedź
Sygnał z tekstem i setInterval uruchomione w konstruktorze klasy (constructor() { … }). Godzinę da new Date().toLocaleTimeString('pl-PL').
Zrób odliczanie od 10 do 0 na sygnale. Po kliknięciu „Start” liczba maleje co sekundę, a po dojściu do zera zatrzymuje się i pojawia się napis „Start!”. Ponowne kliknięcie „Start” w trakcie odliczania nie może uruchomić drugiego stopera.
Kryterium sukcesu: liczby zmieniają się bez klikania; po zerze nic się już nie zmienia, nawet po minucie; szybkie podwójne kliknięcie nie przyspiesza odliczania.
Podpowiedź
setInterval zwraca numer stopera. Zapisz go w polu, a przy zerze wywołaj clearInterval(numer). Przed podwójnym startem chroni pole dziala z przykładu 2.
Zrób stoper z przyciskami „Start”, „Pauza”, „Okrążenie” i „Reset”. „Okrążenie” zapamiętuje aktualny czas w drugim sygnale i pokazuje go pod stoperem jako „Ostatnie okrążenie: 12 s”. „Reset” zatrzymuje stoper i zeruje oba sygnały. Przycisk „Start” ma być zablokowany, gdy stoper działa, a „Pauza” — gdy stoi.
Kryterium sukcesu: pauza zatrzymuje liczenie bez zerowania, start wznawia od tej samej liczby, a stan przycisków zgadza się z tym, czy stoper działa.
Podpowiedź
Stan „czy działa” też jest pokazywany na ekranie (przez [disabled]), więc trzymaj go w sygnale dziala = signal(false). Pauza = clearInterval.
Co trzeba zapamiętać
- Zwykłe pole odświeża się na ekranie tylko przy okazji zdarzenia z szablonu; zmiana ze stopera zostaje niewidoczna.
signal(wartość)tworzy sygnał; wartość początkowa jest obowiązkowa.- Odczyt zawsze z nawiasami:
licznik(), w klasiethis.licznik(), w obiekciedane().pole. setustawia nową wartość i odświeża wszystkie miejsca, które z niej korzystają — niezależnie od tego, kto ją zmienił.- Odczyt bez nawiasów daje napis
[Signal …]i ostrzeżenie NG8109. - Dane, które się zmieniają i są na ekranie, trzymamy w sygnałach.
setIntervalzwraca numer stopera, który zatrzymujeclearInterval.
Sprawdź się: kolega mówi: „u mnie zwykłe pole działa, po co mi sygnał?”. Jakim jednym przykładem pokażesz mu, że się myli?
Dokumentacja: angular.dev — sygnały.