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

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.

Angular 22 update() […lista] filter() { …obiekt } setTimeout 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wybierzesz między set a update w zależności od tego, czy nowa wartość zależy od starej.
  • Zapiszesz funkcję strzałkową n => n + 1 i 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 push na 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.

TEORIA

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

MetodaKiedyPrzykład
setznasz nową wartość; stara nie ma znaczeniathis.licznik.set(0), this.wybrane.set('Druk 3D')
updatenowa wartość zależy od starejthis.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.

TEORIA

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.

ChceszZapis w updateWynik
dodać na końculista => [...lista, 'Ola']nowa tablica
dodać na początkulista => ['Ola', ...lista]nowa tablica
usunąć według wartościlista => lista.filter(x => x !== 'Ola')nowa tablica
wyczyścićset([])nowa, pusta tablica
zmienić jedno pole obiektuw => ({ ...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.

PRZYKŁAD 1

set kontra update

Trzy przyciski: dwa zmieniają licznik o jeden, trzeci zeruje. Zwróć uwagę, gdzie użyto update, a gdzie set.

src/app/nowa-10-1/demo.ts — pełny plik
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);
    }
}
src/app/nowa-10-1/demo.html — pełny plik
<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>
wynik po kliknięciach +1, +1, +1, −1
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?

PRZYKŁAD 2

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.

src/app/nowa-10-2/demo.ts — pełny plik
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]);
    }
}
src/app/nowa-10-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" (click)="dodaj(pole.value); pole.value = ''">Dodaj</button>
  </div>
  <p>Zapisane osoby ({{ osoby().length }}): {{ osoby().join(', ') }}</p>
</div>
wynik po dodaniu imienia „Celina”
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?

PRZYKŁAD 3

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

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

PRZYKŁAD 4

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.

src/app/nowa-10-4/demo.ts — pełny plik
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 }));
    }
}
src/app/nowa-10-4/demo.html — pełny plik
<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>
wynik po kliknięciu „Usuń Bartka” i „Przenieś do sali 7”
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.

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
sygnal.update(stara => nowa)nowa wartość ze starejArgumentem jest funkcja.
signal<string[]>([])sygnał z tablicą tekstówTyp w nawiasach ostrych.
[...lista, x]nowa tablica z dodanym elementemOperator rozproszenia.
lista.filter(x => warunek)nowa tablica z pasującymi elementamiDrugi parametr to numer: (x, nr).
lista.slice(0, -1)nowa tablica bez ostatniego elementuPrzyda się w zadaniach.
{ ...obiekt, pole: x }kopia obiektu ze zmienionym polemW strzałce: ({ … }).
lista.push(x)dopisanie do tej samej tablicyW sygnale — nie używać.
setTimeout(funkcja, ms)kod wykonany raz, po ms milisekundachUdaje czekanie na serwer.
input-grouppole i przycisk sklejone w jeden pasekBootstrap.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
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 typuTypeScript uzna, że to tablica „niczego”, i nie pozwoli niczego do niej dodać. Pisz signal<string[]>([]).
lista.filter(x => x === 'Bartek') do usunięcia BartkaOdwrotnie: zostawi tylko Bartka. Warunek mówi, co zostaje, więc do usunięcia potrzebne !==.
ZADANIA

Zadania

ZAD 1Bez wartości ujemnych★☆☆

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.

ZAD 2Punkty drużyn★☆☆

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

ZAD 3Lista zakupów★★☆

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.

ZAD 4Profil uczestnika★★☆

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

ZAD 5Rezerwacja miejsc★★★

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

ZAD 6Zapis z opóźnieniem — napraw kolegę★★★

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

PODSUMOWANIE

Co trzeba zapamiętać

  • set(x) — gdy nowa wartość nie zależy od starej; update(stara => nowa) — gdy zależy.
  • Argumentem update jest funkcja strzałkowa, nie wartość.
  • Sygnał z tablicą typujesz: signal<string[]>([]).
  • Do tablicy dodajesz przez [...lista, nowy], usuwasz przez filter.
  • Pole obiektu zmieniasz kopią: ({ ...obiekt, pole: nowa }) — z nawiasami wokół klamer.
  • push na 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.

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