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

Serwis i inject() — dane wspólne dla wielu komponentów

Koszyk w sklepie internetowym widać w nagłówku, na liście produktów i na stronie zamówienia. Przepychanie go przez input i output po kilku piętrach komponentów szybko staje się męczące. Dane, które należą do wszystkich, trzymasz w serwisie.

Angular 22 @Service inject() asReadonly() dane współdzielone 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego dwa komponenty z własnymi sygnałami nie widzą wzajemnie swoich danych.
  • Wygenerujesz serwis poleceniem ng generate service i umieścisz w nim sygnał oraz metody.
  • Pobierzesz serwis w komponencie przez inject() i użyjesz go w szablonie.
  • Zabezpieczysz dane serwisu: prywatny sygnał, asReadonly() i metody pilnujące reguł.
  • Zbudujesz koszyk sklepiku z trzech komponentów i jednego serwisu.

Przygotowanie: lekcje 29–32 — komunikacja rodzic–dziecko przez input, output i model; lekcja 08 — #pole. Przewidywany czas: 45 min z zadaniami.

TEORIA

Dane, które należą do wszystkich

Przez cztery lekcje dane zawsze miały właściciela — rodzica — i płynęły w dół do dzieci. To działa, dopóki komponenty tworzą proste drzewo. Gdy dwa komponenty są rodzeństwem albo leżą daleko od siebie, wygodniej oddać dane trzeciej stronie.

Serwis to zwykła klasa z danymi i metodami, której Angular tworzy jeden egzemplarz na całą aplikację. Każdy komponent, który o niego poprosi, dostaje ten sam egzemplarz — więc wszyscy widzą te same dane.

terminal
ng generate service nowa-33-2/zapisy
# CREATE src/app/nowa-33-2/zapisy.spec.ts
# CREATE src/app/nowa-33-2/zapisy.ts
src/app/nowa-33-2/zapisy.ts — plik wygenerowany przez Angular 22
import { Service } from '@angular/core';

@Service()
export class Zapisy {}
KrokKodZnaczenie
1. Klasa serwisu@Service()
export class Zapisy { … }
Angular sam utworzy jeden egzemplarz, gdy ktoś o niego poprosi
2. Komponent prosi o serwisreadonly zapisy = inject(Zapisy);inject z @angular/core zwraca wspólny egzemplarz
3. Komponent używa serwisu{{ zapisy.lista().length }}, this.zapisy.dodaj(imie)zmiana w jednym komponencie od razu widoczna w pozostałych

@Service() i @Injectable({ providedIn: 'root’ })

Dekorator @Service() pojawił się w Angularze 22 i to jego wstawia ng generate service. W starszych projektach, tutorialach i odpowiedziach na forach zobaczysz @Injectable({ providedIn: 'root' }) — w naszych przykładach oba zapisy działają tak samo: jeden wspólny egzemplarz dla całej aplikacji.

Nigdy new

Nie pisz new Zapisy(). Każde new tworzy osobny egzemplarz z osobnymi danymi — dokładnie problem z przykładu 1. Serwis zawsze bierzesz przez inject, przy deklaracji pola klasy.

PRZYKŁAD 1

Problem: każdy komponent ma swoje dane

Dwa komponenty obok siebie: formularz i lista. Każdy ma własny sygnał zapisani. Zobacz, co się dzieje po dodaniu osoby.

src/app/nowa-33-1/formularz.ts — pełny plik
import { Component, signal } from '@angular/core';

@Component({
    selector: 'app-formularz',
    imports: [],
    templateUrl: './formularz.html'
})
export class Formularz {
    readonly zapisani = signal<string[]>([]);

    dodaj(imie: string): void {
        if (imie.trim() !== '') {
            this.zapisani.update(l => [...l, imie.trim()]);
        }
    }
}
src/app/nowa-33-1/formularz.html — pełny plik
<div class="card p-3">
  <h6>Zapisz się</h6>
  <div class="input-group">
    <input #pole class="form-control" placeholder="Imię">
    <button class="btn btn-primary" (click)="dodaj(pole.value); pole.value = ''">Dodaj</button>
  </div>
  <p class="small text-secondary mt-2 mb-0">Formularz widzi zapisanych: {{ zapisani().length }}</p>
</div>
src/app/nowa-33-1/lista.ts — pełny plik
import { Component, signal } from '@angular/core';

@Component({
    selector: 'app-lista',
    imports: [],
    templateUrl: './lista.html'
})
export class Lista {
    readonly zapisani = signal<string[]>([]);
}
src/app/nowa-33-1/lista.html — pełny plik
<div class="card p-3">
  <h6>Lista zapisanych ({{ zapisani().length }})</h6>
  <ul class="mb-0">
    @for (z of zapisani(); track $index) { <li>{{ z }}</li> } @empty { <li class="text-secondary">pusto</li> }
  </ul>
</div>
src/app/nowa-33-1/demo.ts — pełny plik
import { Component } from '@angular/core';
import { Formularz } from './formularz';
import { Lista } from './lista';

@Component({
    selector: 'app-demo',
    imports: [Formularz, Lista],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-33-1/demo.html — pełny plik
<div class="p-3">
  <div class="row g-4" style="max-width: 50rem">
    <div class="col-md-6"><app-formularz /></div>
    <div class="col-md-6"><app-lista /></div>
  </div>
</div>
wynik po dodaniu dwóch osób
Zapisz się                           Lista zapisanych (0)
[Imię        ][Dodaj]                • pusto
Formularz widzi zapisanych: 2

Formularz widzi dwie osoby, a lista jest pusta. Dwa komponenty mają dwa różne sygnały o tej samej nazwie — jak dwóch uczniów z zeszytem podpisanym „Matematyka”, ale z innymi notatkami.

Zatrzymaj się

Te komponenty są rodzeństwem. Jak rozwiązałbyś to przez input i output i co musiałby dopisać rodzic Demo?

PRZYKŁAD 2

Rozwiązanie: wspólny serwis

Dane przenosimy do serwisu Zapisy. Oba komponenty pobierają go przez inject() i nie mają już własnej listy.

src/app/nowa-33-2/zapisy.ts — pełny plik
import { Service, signal } from '@angular/core';

@Service()
export class Zapisy {
    readonly lista = signal<string[]>([]);

    dodaj(imie: string): void {
        if (imie.trim() !== '') {
            this.lista.update(l => [...l, imie.trim()]);
        }
    }
}
src/app/nowa-33-2/formularz.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Zapisy } from './zapisy';

@Component({
    selector: 'app-formularz',
    imports: [],
    templateUrl: './formularz.html'
})
export class Formularz {
    readonly zapisy = inject(Zapisy);
    readonly zapisani = this.zapisy.lista;

    dodaj(imie: string): void {
        this.zapisy.dodaj(imie);
    }
}
src/app/nowa-33-2/formularz.html — pełny plik
<div class="card p-3">
  <h6>Zapisz się <span class="badge text-bg-info">serwis</span></h6>
  <div class="input-group">
    <input #pole class="form-control" placeholder="Imię">
    <button class="btn btn-primary" (click)="dodaj(pole.value); pole.value = ''">Dodaj</button>
  </div>
  <p class="small text-secondary mt-2 mb-0">Formularz widzi zapisanych: {{ zapisani().length }}</p>
</div>
src/app/nowa-33-2/lista.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Zapisy } from './zapisy';

@Component({
    selector: 'app-lista',
    imports: [],
    templateUrl: './lista.html'
})
export class Lista {
    readonly zapisy = inject(Zapisy);
}
src/app/nowa-33-2/lista.html — pełny plik
<div class="card p-3">
  <h6>Lista zapisanych ({{ zapisy.lista().length }}) <span class="badge text-bg-info">serwis</span></h6>
  <ul class="mb-0">
    @for (z of zapisy.lista(); track $index) { <li>{{ z }}</li> } @empty { <li class="text-secondary">pusto</li> }
  </ul>
</div>
src/app/nowa-33-2/demo.ts — pełny plik
import { Component } from '@angular/core';
import { Formularz } from './formularz';
import { Lista } from './lista';

@Component({
    selector: 'app-demo',
    imports: [Formularz, Lista],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-33-2/demo.html — pełny plik
<div class="p-3">
  <h5>Wersja z serwisem</h5>
  <div class="row g-4" style="max-width: 50rem">
    <div class="col-md-6"><app-formularz /></div>
    <div class="col-md-6"><app-lista /></div>
  </div>
</div>
wynik po dodaniu „Ala” i „Ola”
Zapisz się [serwis]                  Lista zapisanych (2) [serwis]
[Imię        ][Dodaj]                • Ala
Formularz widzi zapisanych: 2        • Ola

Teraz każde dodanie od razu pojawia się na liście, a liczby się zgadzają. Rodzic Demo nadal jest pusty — nie przekazuje niczego, bo nie musi. W formularzu zapisani = this.zapisy.lista to nie kopia danych, tylko druga nazwa tego samego sygnału — dzięki temu szablon formularza prawie się nie zmienił. Plik zapisy.spec.ts (testy) możesz usunąć; w laboratorium go nie ma.

Zatrzymaj się

Zamień w liście inject(Zapisy) na new Zapisy(). Co się stanie i dlaczego?

PRZYKŁAD 3

Serwis pilnuje swoich danych

W przykładzie 2 każdy komponent mógł zrobić zapisy.lista.set([]) i wyczyścić listę za plecami serwisu. Tu serwis trzyma sygnał jako private, a na zewnątrz udostępnia go tylko do odczytu przez asReadonly(). Dochodzą computed z limitem miejsc i trzeci komponent — nagłówek.

src/app/nowa-33-3/zapisy.ts — pełny plik
import { Service, computed, signal } from '@angular/core';

@Service()
export class Zapisy {
    readonly limit = 4;
    private readonly _lista = signal<string[]>([]);

    readonly lista = this._lista.asReadonly();
    readonly wolne = computed(() => this.limit - this._lista().length);

    dodaj(imie: string): void {
        if (imie.trim() === '' || this.wolne() === 0) {
            return;
        }
        this._lista.update(l => [...l, imie.trim()]);
    }

    usun(nr: number): void {
        this._lista.update(l => l.filter((z, i) => i !== nr));
    }
}
src/app/nowa-33-3/naglowek.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Zapisy } from './zapisy';

@Component({
    selector: 'app-naglowek',
    imports: [],
    templateUrl: './naglowek.html'
})
export class Naglowek {
    readonly zapisy = inject(Zapisy);
}
src/app/nowa-33-3/naglowek.html — pełny plik
<div class="alert mb-3" [class.alert-success]="zapisy.wolne() > 0" [class.alert-danger]="zapisy.wolne() === 0">
  Warsztat druku 3D — wolnych miejsc: <strong>{{ zapisy.wolne() }}</strong> z {{ zapisy.limit }}
</div>
src/app/nowa-33-3/formularz.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Zapisy } from './zapisy';

@Component({
    selector: 'app-formularz',
    imports: [],
    templateUrl: './formularz.html'
})
export class Formularz {
    readonly zapisy = inject(Zapisy);
}
src/app/nowa-33-3/formularz.html — pełny plik
<div class="card p-3">
  <h6>Zapisz się</h6>
  <div class="input-group">
    <input #pole class="form-control" placeholder="Imię" [disabled]="zapisy.wolne() === 0">
    <button class="btn btn-primary" [disabled]="zapisy.wolne() === 0"
            (click)="zapisy.dodaj(pole.value); pole.value = ''">Dodaj</button>
  </div>
</div>
src/app/nowa-33-3/lista.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Zapisy } from './zapisy';

@Component({
    selector: 'app-lista',
    imports: [],
    templateUrl: './lista.html'
})
export class Lista {
    readonly zapisy = inject(Zapisy);
}
src/app/nowa-33-3/lista.html — pełny plik
<ul class="list-group">
  @for (z of zapisy.lista(); track $index; let nr = $index) {
    <li class="list-group-item d-flex justify-content-between">
      {{ z }}
      <button class="btn btn-sm btn-outline-danger" (click)="zapisy.usun(nr)">Usuń</button>
    </li>
  } @empty {
    <li class="list-group-item text-secondary">Nikt się jeszcze nie zapisał.</li>
  }
</ul>
src/app/nowa-33-3/demo.ts — pełny plik
import { Component } from '@angular/core';
import { Formularz } from './formularz';
import { Lista } from './lista';
import { Naglowek } from './naglowek';

@Component({
    selector: 'app-demo',
    imports: [Naglowek, Formularz, Lista],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-33-3/demo.html — pełny plik
<div class="p-3">
  <div style="max-width: 50rem">
    <app-naglowek />
    <div class="row g-4">
      <div class="col-md-6"><app-formularz /></div>
      <div class="col-md-6"><app-lista /></div>
    </div>
  </div>
</div>
wynik po zapisaniu czterech osób
Warsztat druku 3D — wolnych miejsc: 0 z 4      (czerwony pasek)
[Imię (zablokowane)][Dodaj (zablokowany)]
A [Usuń]   B [Usuń]   C [Usuń]   D [Usuń]

Po czwartej osobie nagłówek robi się czerwony, a pole i przycisk w formularzu są zablokowane. Usuń kogoś — wszystkie trzy komponenty od razu to widzą. Podkreślnik w _lista to tylko zwyczaj: „prywatna wersja pola lista”.

Spróbuj zepsuć

W komponencie listy napisz w metodzie this.zapisy.lista.set([]) — TS2339: Property 'set’ does not exist on type 'Signal<string[]>’. A this.zapisy._lista — TS2341: Property '_lista’ is private and only accessible within class 'Zapisy’. Jedyna droga zmiany danych to metody dodaj i usun, które pilnują limitu.

Zatrzymaj się

Limit sprawdzamy w serwisie (dodaj) i w formularzu ([disabled]). Które zabezpieczenie jest ważniejsze i dlaczego?

PRZYKŁAD 4

Koszyk w sklepiku szkolnym

Trzy miejsca, jeden serwis: lista produktów z przyciskami „Do koszyka”, widok koszyka z usuwaniem i pasek z sumą u rodzica. To układ, który zobaczysz w niemal każdym sklepie internetowym.

src/app/nowa-33-4/koszyk.ts — pełny plik
import { Service, computed, signal } from '@angular/core';

export interface Pozycja {
    nazwa: string;
    cena: number;
    ile: number;
}

@Service()
export class Koszyk {
    private readonly _pozycje = signal<Pozycja[]>([]);

    readonly pozycje = this._pozycje.asReadonly();

    readonly suma = computed(() => {
        let s = 0;
        for (const p of this._pozycje()) {
            s += p.cena * p.ile;
        }
        return s;
    });
    readonly sztuk = computed(() => {
        let n = 0;
        for (const p of this._pozycje()) {
            n += p.ile;
        }
        return n;
    });

    dodaj(nazwa: string, cena: number): void {
        const jest = this._pozycje().find(p => p.nazwa === nazwa);
        if (jest) {
            this._pozycje.update(l => l.map(p => (p.nazwa === nazwa ? { ...p, ile: p.ile + 1 } : p)));
        } else {
            this._pozycje.update(l => [...l, { nazwa, cena, ile: 1 }]);
        }
    }

    usun(nazwa: string): void {
        this._pozycje.update(l => l.filter(p => p.nazwa !== nazwa));
    }
}
src/app/nowa-33-4/produkty.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Koszyk } from './koszyk';

@Component({
    selector: 'app-produkty',
    imports: [],
    templateUrl: './produkty.html'
})
export class Produkty {
    readonly koszyk = inject(Koszyk);
    readonly oferta = [
        { nazwa: 'Drożdżówka', cena: 3.5 },
        { nazwa: 'Woda', cena: 2 },
        { nazwa: 'Kanapka', cena: 7 },
        { nazwa: 'Sok', cena: 4.5 },
    ];
}
src/app/nowa-33-4/produkty.html — pełny plik
<h6>Oferta</h6>
<ul class="list-group">
  @for (p of oferta; track p.nazwa) {
    <li class="list-group-item d-flex justify-content-between align-items-center">
      {{ p.nazwa }} — {{ p.cena.toFixed(2) }} zł
      <button class="btn btn-sm btn-primary" (click)="koszyk.dodaj(p.nazwa, p.cena)">Do koszyka</button>
    </li>
  }
</ul>
src/app/nowa-33-4/widok-koszyka.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Koszyk } from './koszyk';

@Component({
    selector: 'app-widok-koszyka',
    imports: [],
    templateUrl: './widok-koszyka.html'
})
export class WidokKoszyka {
    readonly koszyk = inject(Koszyk);
}
src/app/nowa-33-4/widok-koszyka.html — pełny plik
<h6>Koszyk</h6>
<ul class="list-group">
  @for (p of koszyk.pozycje(); track p.nazwa) {
    <li class="list-group-item d-flex justify-content-between align-items-center">
      {{ p.nazwa }} × {{ p.ile }}
      <button class="btn btn-sm btn-outline-danger" (click)="koszyk.usun(p.nazwa)">✕</button>
    </li>
  } @empty {
    <li class="list-group-item text-secondary">Koszyk jest pusty.</li>
  }
</ul>
src/app/nowa-33-4/demo.ts — pełny plik
import { Component, inject } from '@angular/core';
import { Koszyk } from './koszyk';
import { Produkty } from './produkty';
import { WidokKoszyka } from './widok-koszyka';

@Component({
    selector: 'app-demo',
    imports: [Produkty, WidokKoszyka],
    templateUrl: './demo.html'
})
export class Demo {
    readonly koszyk = inject(Koszyk);
}
src/app/nowa-33-4/demo.html — pełny plik
<div class="p-3">
  <div style="max-width: 56rem">
    <div class="text-bg-dark rounded p-2 mb-3 d-flex justify-content-between">
      <span>Sklepik ZSMI</span>
      <span>🛒 {{ koszyk.sztuk() }} szt. · {{ koszyk.suma().toFixed(2) }} zł</span>
    </div>
    <div class="row g-4">
      <div class="col-md-7"><app-produkty /></div>
      <div class="col-md-5"><app-widok-koszyka /></div>
    </div>
  </div>
</div>
wynik: 2 × drożdżówka, 1 × kanapka
Sklepik ZSMI                      🛒 3 szt. · 14.00 zł
Koszyk:  Drożdżówka × 2  [✕]
         Kanapka × 1     [✕]
(po usunięciu drożdżówek: 🛒 1 szt. · 7.00 zł)

dodaj najpierw szuka produktu w koszyku (find). Jeśli jest — zwiększa jego ile przez map z kopią obiektu; jeśli nie — dopisuje nową pozycję. suma i sztuk to computed z pętlą for…of. Interfejs Pozycja leży w pliku serwisu, bo używa go tylko serwis i jego komponenty.

Krótszy zapis sumy

W cudzym kodzie zamiast pętli zobaczysz często this._pozycje().reduce((s, p) => s + p.ile, 0) — to ta sama pętla zapisana w jednej linii: zacznij od 0 i dodawaj kolejne p.ile.

Zatrzymaj się

Pasek z sumą jest w szablonie rodzica Demo. Czy rodzic przekazuje cokolwiek do app-produkty albo app-widok-koszyka? Skąd pasek wie o zmianach?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenie
ng generate service folder/nazwatworzy plik serwisu (i plik testów .spec.ts)
@Service()klasa jest serwisem; jeden egzemplarz na całą aplikację (Angular 22)
@Injectable({ providedIn: 'root' })starszy zapis o tym samym znaczeniu
readonly x = inject(Serwis)pobranie wspólnego egzemplarza — przy deklaracji pola
private readonly _lista = signal(…)sygnał widoczny tylko wewnątrz serwisu
readonly lista = this._lista.asReadonly()ten sam sygnał udostępniony tylko do odczytu (bez set, update)
computed w serwisiewartości wyliczane widoczne dla wszystkich komponentów
lista.find(p => …)pierwszy pasujący element albo undefined
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
new Zapisy() zamiast inject(Zapisy)Brak błędu, ale każdy komponent ma własny egzemplarz z osobnymi danymi — problem z przykładu 1.
Klasa serwisu bez @Service()Strona pusta, w konsoli przeglądarki: NG0201: No provider found for `Zapisy`. Angular nie wie, że ma utworzyć egzemplarz.
inject(Zapisy) wewnątrz metody, np. dodaj()Po kliknięciu w konsoli: NG0203: … inject() function must be called from an injection context …. Wywołuj inject przy deklaracji pola klasy.
this.zapisy.lista.set([]) przy asReadonly()TS2339: Property 'set’ does not exist on type 'Signal<string[]>’. Zmieniaj dane metodami serwisu.
{{ zapisy.lista.length }}Ostrzeżenie NG8109 lista is a function and should be invoked: lista(). Sygnał z serwisu też czytasz z nawiasami.
Komponenty zmieniają dane serwisu bezpośrednio przez setDziała, ale omija reguły serwisu (np. limit miejsc). Prywatny sygnał + asReadonly() + metody.
ZADANIA

Zadania

ZAD 1Wspólny licznik★☆☆

Serwis Licznik z sygnałem wartosc i metodami plus oraz reset. Dwa komponenty: jeden z przyciskiem „+1”, drugi z wyświetlaczem i przyciskiem „Reset”.

Kryterium sukcesu: kliknięcie w pierwszym komponencie zmienia liczbę w drugim i odwrotnie; rodzic nie ma żadnych pól.

Podpowiedź

Przykład 2 z mniejszą liczbą danych. Serwis wygeneruj poleceniem ng g s.

ZAD 2Motyw aplikacji★☆☆

Serwis Motyw z sygnałem ciemny (true/false) i metodą przelacz(). Komponent nagłówka ma przycisk przełączania, a dwa inne komponenty (karta i stopka) zmieniają wygląd na ciemny, gdy motyw jest włączony.

Kryterium sukcesu: jeden przycisk zmienia wygląd trzech komponentów; żaden z nich nie ma input ani output.

Podpowiedź

W każdym komponencie readonly motyw = inject(Motyw) i [class.text-bg-dark]="motyw.ciemny()".

ZAD 3Powiadomienia★★☆

Serwis Powiadomienia z prywatną listą komunikatów udostępnioną przez asReadonly() i metodami pokaz(tekst) oraz zamknij(nr). Komponent app-panel-powiadomien wyświetla komunikaty jako alert z przyciskiem zamknięcia. Dwa inne komponenty (np. „Zapis na warsztat” i „Zgłoszenie usterki”) wywołują pokaz po kliknięciu swoich przycisków.

Kryterium sukcesu: komunikaty z obu komponentów trafiają do jednego panelu; zamknięcie usuwa właściwy komunikat; spoza serwisu nie da się wywołać set na liście.

Podpowiedź

zamknij jak usun z przykładu 3 — filter po indeksie.

ZAD 4Ulubione wydarzenia★★☆

Serwis Ulubione z listą tytułów i metodą przelacz(tytul) (dodaje albo usuwa). Komponent app-karta (wejście tytul) ma przycisk ☆/★ korzystający z serwisu. Nagłówek pokazuje „Ulubione: 2” i listę tytułów.

Kryterium sukcesu: gwiazdka na karcie odpowiada stanowi serwisu; nagłówek zmienia się po każdym kliknięciu na dowolnej karcie.

Podpowiedź

W serwisie metoda czyUlubione(tytul) zwracająca this._lista().includes(tytul) — wywołana w szablonie karty odświeża się razem z sygnałem.

ZAD 5Biblioteka szkolna★★★

Serwis z listą książek (id, tytul, autor, wypozyczona) udostępnianą przez asReadonly() i metodami wypozycz(id) oraz zwroc(id). Trzy komponenty: katalog (wszystkie książki, przycisk przy dostępnych), „moje wypożyczenia” (z przyciskiem zwrotu) i licznik w nagłówku („dostępnych: X / Y”).

Kryterium sukcesu: wypożyczona książka znika z dostępnych w katalogu i pojawia się w wypożyczeniach; licznik zawsze się zgadza.

Podpowiedź

W serwisie dwa computed: dostepne i wypozyczone (filter). Zmiana jednego pola książki — map z kopią obiektu, jak w lekcji 31.

ZAD 6Kasa w sklepiku z limitem★★★

Rozbuduj przykład 4: produkty mają pole stan (liczba sztuk w magazynie) przechowywane w serwisie. „Do koszyka” jest zablokowany, gdy w koszyku jest już tyle sztuk, ile w magazynie. Dodaj komponent app-kasa z polem „Kwota od klienta” ([(ngModel)]) i wyliczoną resztą oraz przyciskiem „Zapłać”, który czyści koszyk tylko wtedy, gdy kwota wystarcza.

Kryterium sukcesu: nie da się dodać więcej sztuk niż w magazynie; reszta liczy się na bieżąco; „Zapłać” przy za małej kwocie nic nie robi.

Podpowiedź

W serwisie metoda ileWKoszyku(nazwa) i wyczysc(). Kwota od klienta to sygnał w komponencie kasy, a reszta — computed z koszyk.suma().

PODSUMOWANIE

Co trzeba zapamiętać

  • Serwis to klasa z @Service() (dawniej @Injectable({ providedIn: 'root' })); Angular tworzy jeden egzemplarz na całą aplikację.
  • Komponent pobiera serwis przez readonly x = inject(Serwis) przy deklaracji pola — nigdy przez new.
  • Sygnał w serwisie widzą wszystkie komponenty; zmiana w jednym od razu widać w pozostałych.
  • Dane chronisz prywatnym sygnałem, asReadonly() i metodami, które pilnują reguł.
  • input/output — dla rodzica i dziecka; serwis — dla danych wspólnych dla wielu, odległych komponentów.

Sprawdź się: kiedy przekażesz dane przez input/output, a kiedy przez serwis? Podaj po jednym przykładzie z aplikacji szkolnej.

Dokumentacja: angular.dev — Creating and using services.

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