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

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

Angular 22 computed() łańcuch computed push kontra computed pasek postępu 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz wartość wyliczaną z innych sygnałów funkcją computed.
  • Odczytasz computed w szablonie i w klasie — tak samo jak sygnał.
  • Zbudujesz computed, który korzysta z innego computed.
  • Wyjaśnisz, kiedy computed liczy wynik na nowo, a kiedy oddaje zapamiętany.
  • Wyjaśnisz, dlaczego computed nie zauważa zmian zrobionych przez push.
  • Napiszesz computed z blokiem { … return … }, gdy przepis ma kilka kroków.
  • Połączysz computed z 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.

TEORIA

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.

TEORIA

Jak działa computed

schemat
readonly razem = computed(() => this.cena() * this.ile());
//                         ↑ funkcja bez parametrów, która zwraca wynik — „przepis”
CechaZnaczenie
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 odczytuNie 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.

PRZYKŁAD 1

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.

src/app/nowa-11-1/demo.ts — pełny plik
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);
    }
}
src/app/nowa-11-1/demo.html — pełny plik
<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>
wynik po kliknięciach +, + i „Włącz promocję”
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?

PRZYKŁAD 2

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

src/app/nowa-11-2/demo.ts — pełny plik
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()]);
    }
}
src/app/nowa-11-2/demo.html — pełny plik
<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>
wynik po zapisaniu dwóch osób
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ł?

PRZYKŁAD 3

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.

src/app/nowa-11-3/demo.ts — pełny plik
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ść']);
    }
}
src/app/nowa-11-3/demo.html — pełny plik
<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>
wynik
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?

PRZYKŁAD 4

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.

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

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
computed(() => wyrażenie)wartość wyliczana z sygnałówImport z @angular/core.
computed(() => { … return x; })przepis w kilku krokachBez return — pusty wynik.
razem()odczyt wynikuZ nawiasami, jak sygnał.
computed z computedłańcuch wyliczeńNp. ile → wolne → pelne.
Math.round(x)zaokrąglenie do całościW klasie; w szablonie niedostępne.
Math.round(x * 10) / 10zaokrąglenie do 1 miejscaAlbo toFixed(1) w szablonie.
progress, progress-barpasek postępuSzerokość: [style.width.%].
d-flex align-items-center gap-2elementy w rzędzie z odstępemBootstrap.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
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 szablonieNapis w rodzaju [Computed (razem): 15] i ostrzeżenie NG8109 — tak samo jak przy sygnale.
push do tablicy, z której liczy computedWynik 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 = 0Dzielenie przez zero daje Infinity albo NaN. Sprawdź zero w przepisie.
ZADANIA

Zadania

ZAD 1Rabat dla grup★☆☆

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

ZAD 2Długość nazwy★☆☆

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.

ZAD 3Najdłuższe imię★★☆

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

ZAD 4Koszt wyjazdu★★☆

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.

ZAD 5Oceny i średnia★★★

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.

ZAD 6Kasa kina z rzędami★★★

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.

PODSUMOWANIE

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.
  • computed może korzystać z innego computed; nie ma set ani update.
  • Przepis w kilku krokach wymaga klamer i return.
  • push nie zmienia tablicy na nową, więc computed nie 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.

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