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

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

Angular 22 signal() odczyt () set() setInterval 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, kiedy zmiana zwykłego pola klasy nie odświeża ekranu.
  • Uruchomisz kod co sekundę funkcją setInterval i zatrzymasz go przez clearInterval.
  • 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.

TEORIA

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.

TEORIA

Sygnał w trzech krokach

KrokZapisCo znaczy
Tworzeniereadonly licznik = signal(0);sygnał z wartością początkową 0; signal importujesz z @angular/core
Odczytlicznik() w szablonie
this.licznik() w klasie
„daj mi wartość” — zawsze z nawiasami
Zmianathis.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.

PRZYKŁAD 1

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.

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

PRZYKŁAD 2

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.

src/app/nowa-09-2/demo.ts — pełny plik
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);
    }
}
src/app/nowa-09-2/demo.html — pełny plik
<div class="p-3">
  <button class="btn btn-primary" (click)="start()">Start</button>
  <p class="mt-3 fs-4">Stoper: {{ sekundy() }} s</p>
</div>
wynik po kliknięciu Start
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.

PRZYKŁAD 3

Odczyt sygnału — z nawiasami i bez

Najczęstszy błąd początkującego. Zobaczmy, co się dzieje, gdy nawiasów zabraknie.

src/app/nowa-09-3/demo.ts — pełny plik
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');
}
src/app/nowa-09-3/demo.html — pełny plik
<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>
wynik w przeglądarce
Poprawnie: 42
Bez nawiasów: [Signal (punkty): 42]
Tekst: Turniej retro
Obliczenie: 84
ostrzeżenia w terminalu (skrócone)
▲ [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.

PRZYKŁAD 4

Sygnał nie musi trzymać liczby

W sygnale może być tekst, wartość logiczna albo cały obiekt. Zasada odczytu jest zawsze ta sama.

src/app/nowa-09-4/demo.ts — pełny plik
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');
    }
}
src/app/nowa-09-4/demo.html — pełny plik
<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>
wynik po kliknięciu obu przycisków
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.

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
import { signal } from '@angular/core'import funkcjiObok Component.
signal(wartość)nowy sygnałWartość początkowa obowiązkowa.
sygnal()odczyt wartościW klasie: this.sygnal().
sygnal.set(x)ustawienie nowej wartościOdświeża miejsca, które odczytały sygnał.
obiekt().polepole obiektu z sygnałuNajpierw nawiasy, potem kropka.
setInterval(funkcja, ms)kod uruchamiany co ms milisekundZwraca numer stopera.
clearInterval(numer)zatrzymanie stoperaNumer z setInterval.
console.log(…)wypisanie w konsoli przeglądarkiF12 → Console.
fs-4, fs-5większy tekstBootstrap; mniejsza liczba = większy tekst.
NG8109, NG8117sygnał/metoda bez nawiasówOstrzeżenia, nie błędy.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
{{ licznik }} zamiast {{ licznik() }}Napis [Signal (licznik): 0] zamiast liczby i ostrzeżenie NG8109. Zaglądaj do terminala, nie tylko do przeglądarki.
this.licznik = 5Błą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 + 1Błąd typu: dodajesz liczbę do sygnału, a nie do jego wartości. Potrzebne this.licznik() + 1.
Zwykłe pole zmieniane w setInterval albo setTimeoutWartość 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ściBłąd: sygnał zawsze zaczyna od jakiejś wartości — signal(0), signal(''), signal(false).
Brak signal w imporcieCannot find name 'signal’. Dopisz go w import { Component, signal } ….
ZADANIA

Zadania

ZAD 1Licznik w obie strony★☆☆

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.

ZAD 2Tryb cichy★☆☆

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.

ZAD 3Zmień wydarzenie★★☆

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

ZAD 4Zegar na ekranie★★☆

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

ZAD 5Odliczanie do startu★★★

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.

ZAD 6Stoper z pauzą i okrążeniem★★★

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.

PODSUMOWANIE

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 klasie this.licznik(), w obiekcie dane().pole.
  • set ustawia 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.
  • setInterval zwraca numer stopera, który zatrzymuje clearInterval.

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.

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