computed — wartości wyliczane
Cena biletu i liczba biletów to dwa sygnały. Kwota do zapłaty to nie trzeci sygnał, który trzeba pilnować — to coś, co wynika z dwóch pierwszych. Do takich wartości służy computed: piszesz przepis raz, a Angular sam pilnuje, żeby wynik się zgadzał.
Czego się dziś nauczysz
- Utworzysz wartość wyliczaną z innych sygnałów funkcją
computed. - Odczytasz
computedw szablonie i w klasie — tak samo jak sygnał. - Zbudujesz
computed, który korzysta z innegocomputed. - Wyjaśnisz, kiedy
computedliczy wynik na nowo, a kiedy oddaje zapamiętany. - Wyjaśnisz, dlaczego
computednie zauważa zmian zrobionych przezpush. - Napiszesz
computedz blokiem{ … return … }, gdy przepis ma kilka kroków. - Połączysz
computedz wiązaniem[style.width.%]w pasku postępu.
Przygotowanie: lekcje 09–10 — znasz signal, set, update i umiesz tworzyć nową tablicę. Przewidywany czas: 2 × 45 min z zadaniami.
Jedna wartość, dwa miejsca do pilnowania
Wyobraź sobie trzeci sygnał doZaplaty. Za każdym razem, gdy zmienia się cena albo liczba biletów, musisz pamiętać, żeby go przeliczyć. W dwóch metodach pamiętasz, w trzeciej zapominasz — i na ekranie jest zła kwota.
Lepiej powiedzieć raz: „kwota to cena razy liczba biletów” i niech Angular sam pilnuje, żeby to się zgadzało. Arkusz kalkulacyjny działa tak samo: w komórce C1 wpisujesz =A1*B1 i nie przeliczasz wyniku sam — zmieniasz A1, a C1 zmienia się od razu.
Jak działa computed
readonly razem = computed(() => this.cena() * this.ile());
// ↑ funkcja bez parametrów, która zwraca wynik — „przepis”
| Cecha | Znaczenie |
|---|---|
| Przepis, nie wartość | W nawiasie jest funkcja, która zwraca wynik. computed importujesz z @angular/core. |
| Odczyt jak sygnał | {{ razem() }} w szablonie, this.razem() w klasie — z nawiasami. |
| Sam wie, kiedy liczyć | Angular zapamiętuje, które sygnały odczytał przepis. Gdy któryś dostanie nową wartość — wynik liczy się na nowo. Gdy nie — oddaje zapamiętany. |
| Tylko do odczytu | Nie ma set ani update. Chcesz inną kwotę? Zmień cenę albo liczbę biletów. |
Przepis w kilku krokach
Gdy przepis nie mieści się w jednym wyrażeniu, użyj klamer i return, jak w zwykłej funkcji: computed(() => { const w = this.wolne(); if (w === 0) { return 'Brak'; } return 'Są miejsca'; }). Bez return wynik będzie pusty.
Cena razy ilość
Kasa biletowa: liczbę biletów zmieniasz przyciskami, cenę — przyciskiem promocji. Kwota do zapłaty to computed. Klasy d-flex align-items-center gap-2 ustawiają przyciski i liczbę w jednym rzędzie, wyśrodkowane w pionie, z odstępem.
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly cena = signal(15);
readonly ile = signal(1);
readonly razem = computed(() => this.cena() * this.ile());
wiecej(): void {
this.ile.update(n => n + 1);
}
mniej(): void {
this.ile.update(n => (n > 1 ? n - 1 : 1));
}
promocja(): void {
this.cena.set(10);
}
}
<div class="card m-3 p-3" style="width: 22rem">
<p>Cena biletu: {{ cena() }} zł</p>
<div class="d-flex align-items-center gap-2 mb-3">
<button class="btn btn-outline-secondary" (click)="mniej()">−</button>
<span class="fs-5">{{ ile() }}</span>
<button class="btn btn-outline-secondary" (click)="wiecej()">+</button>
</div>
<p class="fs-4">Do zapłaty: <strong>{{ razem() }} zł</strong></p>
<button class="btn btn-warning" (click)="promocja()">Włącz promocję</button>
</div>
Cena biletu: 10 zł
[−] 3 [+]
Do zapłaty: 30 zł
Metoda promocja zmienia tylko cenę — nie ma w niej ani słowa o kwocie — a kwota i tak się przelicza.
Zatrzymaj się
Policz, w ilu metodach musiałbyś przeliczać kwotę, gdyby była zwykłym sygnałem. A w ilu miejscach jest przeliczana teraz?
Kilka wyliczeń z jednej listy
Z listy zapisanych osób wyliczamy trzy rzeczy: ile jest osób, ile zostało miejsc i czy wydarzenie jest pełne. Dane przechowuje tylko jeden sygnał.
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly limit = 4;
readonly osoby = signal<string[]>(['Ala', 'Bartek']);
readonly ile = computed(() => this.osoby().length);
readonly wolne = computed(() => this.limit - this.ile());
readonly pelne = computed(() => this.wolne() === 0);
zapisz(imie: string): void {
if (imie.trim() === '' || this.pelne()) {
return;
}
this.osoby.update(lista => [...lista, imie.trim()]);
}
}
<div class="p-3" style="width: 24rem">
<div class="input-group mb-3">
<input #pole class="form-control" placeholder="Imię">
<button class="btn btn-primary" [disabled]="pelne()"
(click)="zapisz(pole.value); pole.value = ''">Zapisz</button>
</div>
<p>Zapisani: {{ osoby().join(', ') }}</p>
<p>Osób: {{ ile() }} · wolnych miejsc: {{ wolne() }}</p>
<p>Wydarzenie pełne: {{ pelne() ? 'tak' : 'nie' }}</p>
</div>
Zapisani: Ala, Bartek, Celina, Darek
Osób: 4 · wolnych miejsc: 0
Wydarzenie pełne: tak (przycisk Zapisz zablokowany)
wolne korzysta z ile, a pelne z wolne — computed może zależeć od innego computed. Metoda zapisz także odczytuje this.pelne(), żeby nie przyjąć piątej osoby.
Zatrzymaj się
Pole limit jest zwykłą liczbą, nie sygnałem. Kiedy to wystarczy, a kiedy musiałby to być sygnał?
push kontra computed
Wracamy do pułapki z lekcji 10. Tym razem push wywołujemy zwykłym kliknięciem — tam, gdzie „przecież działało”. Obok listy pokazujemy liczbę osób z computed.
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly osoby = signal<string[]>(['Ala']);
readonly ile = computed(() => this.osoby().length);
dodajPush(): void {
this.osoby().push('Gość');
}
dodajNowa(): void {
this.osoby.update(lista => [...lista, 'Gość']);
}
}
<div class="p-3">
<button class="btn btn-outline-danger me-2" (click)="dodajPush()">Dodaj przez push</button>
<button class="btn btn-success" (click)="dodajNowa()">Dodaj przez nową tablicę</button>
<p class="mt-3">Lista: {{ osoby().join(', ') }}</p>
<p>Liczba osób (computed): <strong>{{ ile() }}</strong></p>
</div>
3 × „Dodaj przez push”: Lista: Ala, Gość, Gość, Gość Liczba osób (computed): 1
1 × „Dodaj przez nową tablicę”: Lista: Ala, Gość, Gość, Gość, Gość Liczba osób (computed): 5
computed przelicza się tylko wtedy, gdy sygnał, z którego korzysta, dostanie nową wartość. Po push tablica jest ta sama, więc computed uważa, że jego zapamiętany wynik jest aktualny. Ekran pokazuje dwie sprzeczne informacje naraz.
Zatrzymaj się
Dlaczego lista na ekranie się zmieniła, a liczba nie? Co je różni?
Pasek zapełnienia sali
Coś, co wygląda jak fragment prawdziwej aplikacji: procent zapełnienia jako computed i pasek postępu Bootstrapa (progress i progress-bar), którego szerokość ustawia wiązanie [style.width.%] z lekcji 06.
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly miejsca = signal(30);
readonly zapisani = signal(12);
readonly procent = computed(() => Math.round(this.zapisani() / this.miejsca() * 100));
readonly komunikat = computed(() =>
this.procent() >= 90 ? 'Ostatnie miejsca!' : 'Zapraszamy'
);
zapisz(): void {
this.zapisani.update(n => (n < this.miejsca() ? n + 1 : n));
}
wiekszaSala(): void {
this.miejsca.set(60);
}
}
<div class="card m-3 p-3" style="width: 26rem">
<p>Zapisani: {{ zapisani() }} z {{ miejsca() }} ({{ procent() }}%)</p>
<div class="progress mb-3">
<div class="progress-bar" [style.width.%]="procent()"></div>
</div>
<p class="fw-bold">{{ komunikat() }}</p>
<button class="btn btn-primary mb-2" (click)="zapisz()">Zapisz osobę</button>
<button class="btn btn-outline-secondary" (click)="wiekszaSala()">Przenieś do większej sali</button>
</div>
na starcie: Zapisani: 12 z 30 (40%) Zapraszamy
po 15 zapisach: Zapisani: 27 z 30 (90%) Ostatnie miejsca!
po „Przenieś do większej”: Zapisani: 27 z 60 (45%) Zapraszamy
Jedna zmiana jednego sygnału (miejsca) zmienia procent, długość paska i komunikat. Math.round wolno użyć, bo stoi w klasie, a nie w szablonie.
Zatrzymaj się
Narysuj na kartce strzałki: który sygnał lub computed korzysta z którego. Ile elementów odświeża się po zmianie liczby miejsc?
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
computed(() => wyrażenie) | wartość wyliczana z sygnałów | Import z @angular/core. |
computed(() => { … return x; }) | przepis w kilku krokach | Bez return — pusty wynik. |
razem() | odczyt wyniku | Z nawiasami, jak sygnał. |
| computed z computed | łańcuch wyliczeń | Np. ile → wolne → pelne. |
Math.round(x) | zaokrąglenie do całości | W klasie; w szablonie niedostępne. |
Math.round(x * 10) / 10 | zaokrąglenie do 1 miejsca | Albo toFixed(1) w szablonie. |
progress, progress-bar | pasek postępu | Szerokość: [style.width.%]. |
d-flex align-items-center gap-2 | elementy w rzędzie z odstępem | Bootstrap. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
this.razem.set(100) | Błąd kompilacji: computed nie ma set. Zmień sygnały, z których powstaje. |
computed(this.cena() * this.ile()) | Błąd kompilacji: computed potrzebuje funkcji. Dopisz () => na początku. |
computed(() => { this.cena() * this.ile() }) | Wynik zawsze pusty. W klamrach trzeba napisać return — albo usunąć klamry. |
{{ razem }} w szablonie | Napis w rodzaju [Computed (razem): 15] i ostrzeżenie NG8109 — tak samo jak przy sygnale. |
push do tablicy, z której liczy computed | Wynik się nie zmienia, choć lista na ekranie urosła. Twórz nową tablicę. |
readonly razem = this.cena() * this.ile(); | Liczba policzona raz, przy tworzeniu komponentu. Zmiana ceny jej nie zmieni. |
zapisani() / miejsca() przy miejsca = 0 | Dzielenie przez zero daje Infinity albo NaN. Sprawdź zero w przepisie. |
Zadania
W przykładzie 1 dodaj computed o nazwie rabat: 10% kwoty, gdy biletów jest 5 lub więcej, w przeciwnym razie 0. Pokaż rabat i kwotę po rabacie (trzeci computed).
Kryterium sukcesu: przy 4 biletach rabat wynosi 0; przy 5 pojawia się 10% kwoty, a kwota po rabacie maleje.
Podpowiedź
computed(() => this.ile() >= 5 ? this.razem() * 0.1 : 0).
Zrób sygnał z nazwą wydarzenia ustawianą z pola #pole przyciskiem „Ustaw”. Dodaj trzy computed: nazwę wielkimi literami, liczbę znaków bez spacji na brzegach i informację „za długa” albo „w porządku” (limit 20 znaków).
Kryterium sukcesu: po każdym ustawieniu nazwy zmieniają się wszystkie trzy wyniki, a metoda „Ustaw” zawiera tylko jedno set.
Podpowiedź
Liczba znaków: this.nazwa().trim().length.
W przykładzie 2 dodaj computed, który pokazuje najdłuższe imię na liście, i drugi, który pokazuje wszystkie imiona wielkimi literami, rozdzielone przecinkami.
Kryterium sukcesu: po dopisaniu dłuższego imienia oba napisy zmieniają się bez dopisywania czegokolwiek do metody zapisz.
Podpowiedź
W pierwszym użyj klamer, pętli for…of i return. Wielkie litery: this.osoby().map(i => i.toUpperCase()).join(', ').
Trzy sygnały: długość trasy w km, spalanie w litrach na 100 km i cena litra paliwa, każdy zmieniany przyciskami „−” i „+”. computed liczy zużyte litry, drugi computed — koszt z dwoma miejscami po kropce, a trzeci — koszt na osobę przy 4 osobach.
Kryterium sukcesu: każde kliknięcie zmienia wszystkie trzy wyniki; w metodach przycisków nie ma żadnego przeliczania kosztu.
Podpowiedź
Litry = km * spalanie / 100. Formatowanie toFixed(2) zrób w szablonie.
Sygnał przechowuje tablicę ocen ucznia (liczby 1–6). Przyciski „1” … „6” dopisują ocenę, „Cofnij” usuwa ostatnią. computed liczą: średnią z dwoma miejscami po kropce, najwyższą ocenę, liczbę ocen niedostatecznych i komunikat „zagrożenie” (średnia poniżej 2) albo „w porządku”. Przy pustej liście średnia ma pokazywać „brak ocen”, a nie NaN.
Kryterium sukcesu: po każdym kliknięciu zmieniają się wszystkie cztery wyniki, a pusta lista nie daje NaN ani -Infinity.
Podpowiedź
Suma w pętli for…of w computed z klamrami; przy length === 0 zwróć wcześniej. Najwyższa: Math.max(...this.oceny()), ale tylko dla niepustej tablicy.
Sala ma 5 rzędów po 8 miejsc. Sygnał przechowuje tablicę numerów zajętych miejsc (np. [3, 4, 17]). Pole #pole i przycisk „Rezerwuj” dodają numer, jeśli jest z zakresu 1–40 i nie jest zajęty; w przeciwnym razie pokaż komunikat. computed liczą: liczbę wolnych miejsc, procent zajętości (pasek postępu), utarg przy cenie 18 zł i informację, czy sala jest pełna (wtedy przycisk zablokowany).
Kryterium sukcesu: nie da się zarezerwować tego samego miejsca dwa razy ani numeru spoza zakresu; pasek, utarg i liczba wolnych miejsc zgadzają się po każdej rezerwacji.
Podpowiedź
Sprawdzenie zajętości: this.zajete().includes(nr). Numer z pola: Number(pole.value). Komunikat trzymaj w sygnale, bo jest na ekranie.
Co trzeba zapamiętać
computed(() => wyrażenie)tworzy wartość wyliczaną z innych sygnałów; odczyt z nawiasami.- Wynik przelicza się sam, gdy zmieni się którykolwiek użyty sygnał — i tylko wtedy.
computedmoże korzystać z innegocomputed; nie masetaniupdate.- Przepis w kilku krokach wymaga klamer i
return. pushnie zmienia tablicy na nową, więccomputednie zauważy zmiany.- Zwykłe pole wyliczone z sygnałów liczy się tylko raz — do wartości zależnych służy
computed.
Sprawdź się: czym różni się readonly razem = this.cena() * this.ile(); od readonly razem = computed(() => this.cena() * this.ile());? Co pokaże ekran po zmianie ceny w każdej wersji?
Dokumentacja: angular.dev — sygnały computed.