set i update — zmiana wartości sygnału
Zapis this.licznik.set(this.licznik() + 1) działa, ale nazwa sygnału pojawia się w nim dwa razy. Poznasz update — krótszy sposób na „weź starą wartość i policz nową” — i nauczysz się bezpiecznie zmieniać tablice i obiekty w sygnale.
Czego się dziś nauczysz
- Wybierzesz między
setaupdatew zależności od tego, czy nowa wartość zależy od starej. - Zapiszesz funkcję strzałkową
n => n + 1i wyjaśnisz, co oznacza jej parametr. - Określisz typ sygnału z tablicą zapisem
signal<string[]>([]). - Dodasz element do tablicy w sygnale, tworząc nową tablicę
[...lista, nowy]. - Usuniesz elementy z tablicy metodą
filter. - Zmienisz jedno pole obiektu w sygnale kopią
{ ...obiekt, pole: nowa }. - Wyjaśnisz, dlaczego
pushna tablicy w sygnale jest pułapką.
Przygotowanie: lekcja 09 — umiesz utworzyć sygnał, odczytać go z nawiasami i ustawić przez set. Przewidywany czas: 2 × 45 min z zadaniami.
Nowa wartość z poprzedniej
Większość zmian w aplikacji wygląda tak samo: weź to, co jest, i trochę zmień. Licznik o jeden więcej. Lista z jedną pozycją więcej. To samo wydarzenie, ale w innej sali. Za każdym razem potrzebujesz starej wartości, żeby policzyć nową.
| Metoda | Kiedy | Przykład |
|---|---|---|
set | znasz nową wartość; stara nie ma znaczenia | this.licznik.set(0), this.wybrane.set('Druk 3D') |
update | nowa wartość zależy od starej | this.licznik.update(n => n + 1) |
W nawiasie update stoi funkcja strzałkowa: po lewej od => nazwa dla starej wartości (tu n), po prawej wyrażenie, którego wynik stanie się nową wartością. Nazwa jest dowolna — równie dobrze napiszesz stara => stara + 1. Angular sam wywoła tę funkcję i poda jej aktualną wartość sygnału.
Zasada: nowe pudełko zamiast przemeblowania starego
Sygnał sprawdza, czy dostał inną wartość niż miał. Dla liczby czy tekstu to proste. Dla tablicy i obiektu „inna” znaczy „inny obiekt w pamięci”. Jeśli do starej tablicy dopiszesz element metodą push, to wciąż jest ta sama tablica — sygnał uzna, że nic się nie zmieniło, i nikogo nie powiadomi.
| Chcesz | Zapis w update | Wynik |
|---|---|---|
| dodać na końcu | lista => [...lista, 'Ola'] | nowa tablica |
| dodać na początku | lista => ['Ola', ...lista] | nowa tablica |
| usunąć według wartości | lista => lista.filter(x => x !== 'Ola') | nowa tablica |
| wyczyścić | set([]) | nowa, pusta tablica |
| zmienić jedno pole obiektu | w => ({ ...w, sala: 7 }) | nowy obiekt |
Trzy kropki ... to operator rozproszenia. [...lista, 'X'] znaczy: „nowa tablica ze wszystkimi elementami lista, a na końcu 'X'”. { ...w, sala: 7 } — „kopia obiektu w, ale z salą 7”. filter zostawia te elementy, dla których warunek jest prawdziwy, i zawsze zwraca nową tablicę.
Nawiasy wokół obiektu
W w => ({ ...w, sala: 7 }) nawiasy okrągłe wokół klamer są obowiązkowe. Bez nich TypeScript uzna klamrę za początek bloku kodu funkcji, a nie za obiekt.
set kontra update
Trzy przyciski: dwa zmieniają licznik o jeden, trzeci zeruje. Zwróć uwagę, gdzie użyto update, a gdzie set.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly zapisanych = signal(0);
dodaj(): void {
this.zapisanych.update(n => n + 1);
}
odejmij(): void {
this.zapisanych.update(n => n - 1);
}
wyzeruj(): void {
this.zapisanych.set(0);
}
}
<div class="p-3">
<p class="fs-4">Zapisanych: {{ zapisanych() }}</p>
<button class="btn btn-success me-2" (click)="dodaj()">+1</button>
<button class="btn btn-warning me-2" (click)="odejmij()">−1</button>
<button class="btn btn-outline-danger" (click)="wyzeruj()">Zeruj</button>
</div>
Zapisanych: 2
Licznik może zejść poniżej zera — tym zajmiesz się w zadaniu 1.
Zatrzymaj się
Czy wyzeruj dałoby się napisać przez update? Da się: update(n => 0). Dlaczego mimo to set(0) jest czytelniejsze?
Dodawanie do listy — nowa tablica
Lista zapisanych osób. Imię wpisujesz w pole #pole (lekcja 08) i klikasz Dodaj. Pole i przycisk są sklejone klasą Bootstrapa input-group.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly osoby = signal<string[]>(['Ala', 'Bartek']);
dodaj(imie: string): void {
const czyste = imie.trim();
if (czyste === '') {
return;
}
this.osoby.update(lista => [...lista, czyste]);
}
}
<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" (click)="dodaj(pole.value); pole.value = ''">Dodaj</button>
</div>
<p>Zapisane osoby ({{ osoby().length }}): {{ osoby().join(', ') }}</p>
</div>
Zapisane osoby (3): Ala, Bartek, Celina
Zapis signal<string[]>([...]) mówi TypeScriptowi: „w tym sygnale zawsze będzie tablica tekstów”. Spróbuj dodać same spacje — nic się nie stanie, bo trim() zamienia je na pusty tekst.
Zatrzymaj się
Zamień w metodzie czyste na liczbę 5 ([...lista, 5]). Co zgłosi kompilator i dlaczego to dobrze?
Pułapka z push
Zapis na wydarzenie często trwa chwilę — dane lecą do serwera. Udajemy to funkcją setTimeout, która wykona kod raz, po sekundzie. Oba przyciski dopisują osobę z opóźnieniem: pierwszy przez push, drugi przez nową tablicę.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly osoby = signal<string[]>(['Ala']);
zapiszPush(): void {
setTimeout(() => {
this.osoby().push('Gość (push)'); // ta sama tablica
}, 1000);
}
zapiszNowa(): void {
setTimeout(() => {
this.osoby.update(lista => [...lista, 'Gość (nowa)']); // nowa tablica
}, 1000);
}
}
<div class="p-3">
<button class="btn btn-outline-danger me-2" (click)="zapiszPush()">Zapisz przez push</button>
<button class="btn btn-success" (click)="zapiszNowa()">Zapisz przez nową tablicę</button>
<p class="mt-3">Zapisani: {{ osoby().join(', ') }}</p>
</div>
Zapisz przez push, czekasz 3 s: Zapisani: Ala
Zapisz przez nową tablicę — od razu: Zapisani: Ala, Gość (push)
… i po sekundzie: Zapisani: Ala, Gość (push), Gość (nowa)
Po push nic się nie dzieje. Kliknięcie drugiego przycisku odświeża ekran — i wtedy wychodzi na jaw gość, który od dawna siedział w tablicy, tylko nikt o nim ekranowi nie powiedział. Po sekundzie dochodzi wpis z nowej tablicy — już bez żadnego kliknięcia.
Najgorszy rodzaj błędu
Gdyby push stał wprost w metodzie od kliknięcia, wpis by się pokazał — kliknięcie odświeża ekran. Taki kod działa w prostym przykładzie i psuje się później. W lekcji 11 zobaczysz, że computed nie da się tak oszukać nawet przy kliknięciu.
Usuwanie z listy i zmiana pola obiektu
Dwie operacje, które wykonasz w każdym projekcie: usuwanie metodą filter i zmiana jednego pola obiektu kopią z operatorem rozproszenia.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly osoby = signal<string[]>(['Ala', 'Bartek', 'Celina', 'Darek']);
readonly wydarzenie = signal({ tytul: 'Noc robotów', sala: 12, miejsca: 20 });
usunPierwsza(): void {
this.osoby.update(lista => lista.filter((imie, nr) => nr !== 0));
}
usunBartka(): void {
this.osoby.update(lista => lista.filter(imie => imie !== 'Bartek'));
}
przeniesDoSali7(): void {
this.wydarzenie.update(w => ({ ...w, sala: 7 }));
}
}
<div class="card m-3 p-3" style="width: 26rem">
<h5>{{ wydarzenie().tytul }}</h5>
<p>Sala {{ wydarzenie().sala }}, miejsc: {{ wydarzenie().miejsca }}</p>
<p>Zapisani: {{ osoby().join(', ') }}</p>
<button class="btn btn-outline-danger mb-2" (click)="usunPierwsza()">Usuń pierwszą osobę</button>
<button class="btn btn-outline-danger mb-2" (click)="usunBartka()">Usuń Bartka</button>
<button class="btn btn-outline-primary" (click)="przeniesDoSali7()">Przenieś do sali 7</button>
</div>
Noc robotów
Sala 7, miejsc: 20
Zapisani: Ala, Celina, Darek
Przy przeniesieniu zmienia się tylko numer sali — tytuł i liczba miejsc zostają, bo ...w skopiowało je ze starego obiektu. W filter((imie, nr) => nr !== 0) drugi parametr to numer elementu: „zostaw wszystkie oprócz pierwszego”.
Zatrzymaj się
Co by się stało, gdybyś napisał w => ({ sala: 7 }), bez ...w? Sprawdź, co powie kompilator.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
sygnal.update(stara => nowa) | nowa wartość ze starej | Argumentem jest funkcja. |
signal<string[]>([]) | sygnał z tablicą tekstów | Typ w nawiasach ostrych. |
[...lista, x] | nowa tablica z dodanym elementem | Operator rozproszenia. |
lista.filter(x => warunek) | nowa tablica z pasującymi elementami | Drugi parametr to numer: (x, nr). |
lista.slice(0, -1) | nowa tablica bez ostatniego elementu | Przyda się w zadaniach. |
{ ...obiekt, pole: x } | kopia obiektu ze zmienionym polem | W strzałce: ({ … }). |
lista.push(x) | dopisanie do tej samej tablicy | W sygnale — nie używać. |
setTimeout(funkcja, ms) | kod wykonany raz, po ms milisekundach | Udaje czekanie na serwer. |
input-group | pole i przycisk sklejone w jeden pasek | Bootstrap. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
this.licznik.update(this.licznik() + 1) | Błąd kompilacji: update oczekuje funkcji, a dostał liczbę. Popraw na update(n => n + 1). |
this.osoby().push(x) | Czasem działa (przy kliknięciu), czasem nie (stoper, serwer, computed). Zawsze twórz nową tablicę. |
update(w => { ...w, sala: 7 }) | Błąd kompilacji — klamra potraktowana jako blok kodu. Otocz obiekt nawiasami: ({ ...w, sala: 7 }). |
update(w => ({ sala: 7 })) | Obiekt traci pozostałe pola; kompilator zgłosi, że brakuje tytul i miejsca. Pamiętaj o ...w. |
signal([]) bez typu | TypeScript uzna, że to tablica „niczego”, i nie pozwoli niczego do niej dodać. Pisz signal<string[]>([]). |
lista.filter(x => x === 'Bartek') do usunięcia Bartka | Odwrotnie: zostawi tylko Bartka. Warunek mówi, co zostaje, więc do usunięcia potrzebne !==. |
Zadania
W przykładzie 1 popraw metodę odejmij tak, żeby licznik nigdy nie zszedł poniżej zera. Dodaj też przycisk „+5”.
Kryterium sukcesu: wielokrotne kliknięcie „−1” przy zerze zostawia 0, a „+5” dodaje pięć jednym kliknięciem.
Podpowiedź
W funkcji strzałkowej użyj operatora warunkowego: n => n > 0 ? n - 1 : 0.
Zrób tablicę wyników dwóch drużyn (dwa sygnały). Każda drużyna ma przyciski „+1” i „+3”, a pod spodem jest przycisk „Nowy mecz”, który zeruje oba wyniki. Pokaż wynik w formie „Orły 4 : 3 Smoki”.
Kryterium sukcesu: przyciski punktów używają update, „Nowy mecz” — set, a wynik zmienia się natychmiast.
Podpowiedź
Jedna metoda z dwoma argumentami może obsłużyć wszystkie przyciski punktów, np. dodaj('orly', 3).
Zrób listę zakupów: pole tekstowe, przycisk „Dodaj”, przycisk „Usuń ostatni” i przycisk „Wyczyść”. Pokaż liczbę pozycji i całą listę po przecinku.
Kryterium sukcesu: każdy przycisk działa; nie da się dodać pustej pozycji; „Wyczyść” używa set, pozostałe update.
Podpowiedź
lista.slice(0, -1) zwraca nową tablicę bez ostatniego elementu.
Sygnał przechowuje obiekt { imie: 'Ola', klasa: '3TP', punkty: 0 }. Zrób przyciski: „+10 punktów”, „Przenieś do 4TP” i „Zmień imię” (nowe imię z pola #pole). Każdy przycisk zmienia tylko jedno pole obiektu.
Kryterium sukcesu: po każdym kliknięciu zmienia się dokładnie jedno pole, a pozostałe zostają bez zmian.
Podpowiedź
Trzy razy ten sam wzór: update(u => ({ ...u, punkty: u.punkty + 10 })).
Sygnał przechowuje obiekt { tytul, miejsca, zapisani }, gdzie zapisani to tablica imion. Przycisk „Zapisz” dopisuje imię z pola i zmniejsza miejsca o jeden — w jednym wywołaniu update. Przycisk „Wypisz ostatniego” robi odwrotnie. Przy zerze miejsc zapis nic nie robi, a przycisk jest zablokowany.
Kryterium sukcesu: po zapisie zmieniają się jednocześnie lista i liczba miejsc; przy zerze miejsc przycisk jest nieaktywny.
Podpowiedź
Wewnątrz jednej kopii obiektu możesz nadpisać dwa pola: ({ ...w, miejsca: w.miejsca - 1, zapisani: [...w.zapisani, imie] }).
Kolega napisał „zapis do serwera” tak: setTimeout(() => { this.osoby().push(imie); this.komunikat = 'Zapisano'; }, 1500);, gdzie osoby to sygnał z tablicą, a komunikat — zwykłe pole. Odtwórz ten kod, sprawdź, co widać na ekranie po 2 sekundach, i opisz w komentarzu oba błędy. Potem popraw kod tak, żeby po 1,5 s pojawiały się i nowe imię, i komunikat — bez żadnego klikania.
Kryterium sukcesu: w wersji poprawionej po 1,5 s widać nowe imię i komunikat; komentarz wymienia dwie przyczyny błędu.
Podpowiedź
Dwa problemy, dwie lekcje: push zamiast nowej tablicy (ta lekcja) i zwykłe pole zmieniane w setTimeout (lekcja 09).
Co trzeba zapamiętać
set(x)— gdy nowa wartość nie zależy od starej;update(stara => nowa)— gdy zależy.- Argumentem
updatejest funkcja strzałkowa, nie wartość. - Sygnał z tablicą typujesz:
signal<string[]>([]). - Do tablicy dodajesz przez
[...lista, nowy], usuwasz przezfilter. - Pole obiektu zmieniasz kopią:
({ ...obiekt, pole: nowa })— z nawiasami wokół klamer. pushna tablicy w sygnale zmienia zawartość, ale sygnał tego nie widzi.
Sprawdź się: dlaczego this.osoby().push('Ola') w metodzie od kliknięcia „działa”, a ten sam kod w setTimeout już nie?
Dokumentacja: angular.dev — zapisywalne sygnały.