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

Cykl życia komponentu: ngOnInit, ngOnChanges, ngOnDestroy

Komponent rodzi się, dostaje dane od rodzica, zmienia je i w końcu znika. Angular w każdym z tych momentów wywołuje metody o ustalonych nazwach. Poznasz trzy najważniejsze i dowiesz się, kiedy wystarczą computed i effect, a kiedy potrzebny jest ngOnInit.

Angular 22 ngOnInit ngOnChanges ngOnDestroy OnInit SimpleChanges NG8118 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wymienisz kolejność: constructor → ngOnChanges → ngOnInit → … → ngOnDestroy.
  • Dopiszesz do klasy interfejsy OnInit, OnChanges, OnDestroy i odpowiadające im metody.
  • Wyjaśnisz, dlaczego wejścia nie mają jeszcze wartości w konstruktorze, i odczytasz błąd NG8118.
  • Użyjesz ngOnInit do jednorazowego przygotowania komponentu na podstawie wejścia.
  • Odczytasz poprzednią wartość wejścia w ngOnChanges przez SimpleChanges.
  • Wybierzesz właściwe narzędzie: computed, effect, DestroyRef albo metoda cyklu życia.

Przygotowanie: lekcje 29–32 (input, output), lekcja 33 (serwis), lekcja 37 (effect, setInterval, DestroyRef). Przewidywany czas: 45 min z zadaniami.

TEORIA

Od narodzin do usunięcia

Gdy rodzic ma w szablonie <app-karta [tytul]="…" />, Angular najpierw tworzy obiekt klasy Karta (konstruktor), dopiero potem przekazuje mu wartości wejść, a na końcu wyświetla szablon. Później wejścia mogą się zmieniać, aż w końcu @if albo router usunie kartę. W każdym z tych momentów Angular wywołuje metodę o ustalonej nazwie — jeśli ją napisałeś.

MetodaInterfejsKiedy Angular ją wywołuje
constructor()—przy tworzeniu obiektu; wejścia nie mają jeszcze wartości od rodzica
ngOnChanges(zmiany)OnChangesprzed ngOnInit i potem przy każdej zmianie wartości wejścia przez rodzica
ngOnInit()OnInitraz, gdy wejścia mają już pierwsze wartości
ngOnDestroy()OnDestroyraz, tuż przed usunięciem komponentu
szkielet komponentu z metodami cyklu życia
import { Component, OnChanges, OnDestroy, OnInit, SimpleChanges, input } from '@angular/core';

export class Karta implements OnChanges, OnInit, OnDestroy {
    readonly tytul = input.required<string>();

    ngOnChanges(zmiany: SimpleChanges<Karta>): void { … }
    ngOnInit(): void { … }
    ngOnDestroy(): void { … }
}

implements OnInit to obietnica złożona kompilatorowi: „ta klasa ma metodę ngOnInit”. Angular wywołałby metodę i bez interfejsu, ale z interfejsem kompilator wyłapie literówkę w nazwie.

A co już znasz?

Większość tego, co w starszym kodzie robi się w tych metodach, w kursie robiliśmy sygnałami: wartość wyliczana z wejścia to computed (przelicza się sama, bez ngOnChanges), sprzątanie to DestroyRef.onDestroy z lekcji 37 (to samo co ngOnDestroy). Metody cyklu życia są potrzebne tam, gdzie sygnały nie wystarczają — i spotkasz je w prawie każdym istniejącym projekcie.

PRZYKŁAD 1

Kolejność wywołań na żywo

Karta zapisuje każde wywołanie metody cyklu życia do dziennika — serwisu z sygnałem-tablicą (lekcja 33). Rodzic pokazuje dziennik, zmienia tytuł karty i chowa ją przez @if.

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

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

    dodaj(tekst: string): void {
        this.wpisy.update(l => [...l, tekst]);
    }

    wyczysc(): void {
        this.wpisy.set([]);
    }
}
src/app/nowa-38-1/karta.ts — pełny plik
import { Component, OnChanges, OnDestroy, OnInit, SimpleChanges, inject, input } from '@angular/core';
import { Dziennik } from './dziennik';

@Component({
    selector: 'app-karta',
    imports: [],
    templateUrl: './karta.html'
})
export class Karta implements OnChanges, OnInit, OnDestroy {
    private readonly dziennik = inject(Dziennik);
    readonly tytul = input.required<string>();

    constructor() {
        this.dziennik.dodaj('constructor — wejście jeszcze bez wartości');
    }

    ngOnChanges(zmiany: SimpleChanges<Karta>): void {
        const z = zmiany.tytul;
        if (z) {
            this.dziennik.dodaj('ngOnChanges — tytuł: ' + z.previousValue + ' → ' + z.currentValue);
        }
    }

    ngOnInit(): void {
        this.dziennik.dodaj('ngOnInit — start z tytułem „' + this.tytul() + '”');
    }

    ngOnDestroy(): void {
        this.dziennik.dodaj('ngOnDestroy — karta znika');
    }
}
src/app/nowa-38-1/karta.html — pełny plik
<div class="card p-3 mb-3" style="width: 22rem">
  <h5 class="mb-0">{{ tytul() }}</h5>
</div>
src/app/nowa-38-1/demo.ts — pełny plik
import { Component, inject, signal } from '@angular/core';
import { Karta } from './karta';
import { Dziennik } from './dziennik';

@Component({
    selector: 'app-demo',
    imports: [Karta],
    templateUrl: './demo.html'
})
export class Demo {
    readonly dziennik = inject(Dziennik);
    readonly tytuly = ['Turniej retro', 'Noc robotów', 'Warsztat druku 3D'];
    readonly nr = signal(0);
    readonly widoczna = signal(true);

    nastepny(): void {
        this.nr.update(n => (n + 1) % this.tytuly.length);
    }
}
src/app/nowa-38-1/demo.html — pełny plik
<div class="p-3" style="max-width: 36rem">
  <button class="btn btn-primary me-2" (click)="nastepny()">Zmień tytuł</button>
  <button class="btn btn-outline-primary me-2" (click)="widoczna.set(!widoczna())">
    {{ widoczna() ? 'Ukryj kartę' : 'Pokaż kartę' }}
  </button>
  <button class="btn btn-outline-secondary" (click)="dziennik.wyczysc()">Wyczyść dziennik</button>

  <div class="mt-3">
    @if (widoczna()) {
      <app-karta [tytul]="tytuly[nr()]" />
    }
  </div>

  <ol class="list-group list-group-numbered small">
    @for (w of dziennik.wpisy(); track $index) {
      <li class="list-group-item font-monospace">{{ w }}</li>
    }
  </ol>
</div>
dziennik po: otwarciu, „Zmień tytuł”, „Ukryj kartę”, „Pokaż kartę”
1. constructor — wejście jeszcze bez wartości
2. ngOnChanges — tytuł: undefined → Turniej retro
3. ngOnInit — start z tytułem „Turniej retro”
4. ngOnChanges — tytuł: Turniej retro → Noc robotów
5. ngOnDestroy — karta znika
6. constructor — wejście jeszcze bez wartości
7. ngOnChanges — tytuł: undefined → Noc robotów
8. ngOnInit — start z tytułem „Noc robotów”

Zmiana tytułu wywołuje tylko ngOnChanges — ngOnInit działa raz na całe życie komponentu. Schowanie karty to ngOnDestroy, a ponowne pokazanie — zupełnie nowy obiekt, który przechodzi całą drogę od konstruktora. SimpleChanges<Karta> to obiekt z polem dla każdego wejścia, które się zmieniło; zmiany.tytul ma previousValue, currentValue i firstChange.

Zatrzymaj się

Dlaczego w ngOnChanges jest if (z)? Kiedy zmiany.tytul mogłoby nie istnieć, gdyby karta miała dwa wejścia?

PRZYKŁAD 2

ngOnInit: przygotowanie na podstawie wejścia

Edytor nicku dostaje od rodzica obecny nick, ale pozwala go zmieniać u siebie, w roboczej kopii. Rodzic dowiaduje się o nowej wartości dopiero po „Zapisz”. Roboczą kopię trzeba raz, na starcie, wypełnić wartością z wejścia — to typowe zadanie dla ngOnInit.

src/app/nowa-38-2/edytor.ts — pełny plik
import { Component, OnInit, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-edytor',
    imports: [FormsModule],
    templateUrl: './edytor.html'
})
export class Edytor implements OnInit {
    readonly nick = input.required<string>();
    readonly zapisano = output<string>();
    readonly anulowano = output<void>();

    readonly roboczy = signal('');

    ngOnInit(): void {
        this.roboczy.set(this.nick());
    }

    zapisz(): void {
        if (this.roboczy().trim().length >= 3) {
            this.zapisano.emit(this.roboczy().trim());
        }
    }
}
src/app/nowa-38-2/edytor.html — pełny plik
<div class="input-group input-group-sm">
  <input name="nick" class="form-control" [(ngModel)]="roboczy" (keyup.enter)="zapisz()">
  <button class="btn btn-success" type="button" [disabled]="roboczy().trim().length < 3" (click)="zapisz()">Zapisz</button>
  <button class="btn btn-outline-secondary" type="button" (click)="roboczy.set(nick())">Cofnij zmiany</button>
  <button class="btn btn-outline-secondary" type="button" (click)="anulowano.emit()">✕</button>
</div>
src/app/nowa-38-2/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { Edytor } from './edytor';

@Component({
    selector: 'app-demo',
    imports: [Edytor],
    templateUrl: './demo.html'
})
export class Demo {
    readonly gracze = signal([
        { id: 1, nick: 'Pixel' },
        { id: 2, nick: 'Bajt' },
        { id: 3, nick: 'Kursor' },
    ]);
    readonly edytowany = signal(0);

    zmien(id: number, nowy: string): void {
        this.gracze.update(l => l.map(g => (g.id === id ? { ...g, nick: nowy } : g)));
        this.edytowany.set(0);
    }
}
src/app/nowa-38-2/demo.html — pełny plik
<div class="p-3" style="max-width: 32rem">
  <ul class="list-group">
    @for (g of gracze(); track g.id) {
      <li class="list-group-item">
        @if (edytowany() === g.id) {
          <app-edytor [nick]="g.nick" (zapisano)="zmien(g.id, $event)" (anulowano)="edytowany.set(0)" />
        } @else {
          <div class="d-flex justify-content-between align-items-center">
            {{ g.nick }}
            <button class="btn btn-sm btn-outline-primary" (click)="edytowany.set(g.id)">Edytuj</button>
          </div>
        }
      </li>
    }
  </ul>
</div>
edycja drugiego gracza
Pixel                         [Edytuj]
[Bajtek     ] [Zapisz] [Cofnij zmiany] [✕]
Kursor                        [Edytuj]
(po „Zapisz”: Bajtek na liście, edytor znika)

Dlaczego nie w konstruktorze? Spróbuj przenieść this.roboczy.set(this.nick()) do constructor():

terminal
✘ [ERROR] NG8118: `nick` is a required `input` and does not have a value in this context.

Kompilator Angulara 22 sam wyłapuje odczyt wejścia obowiązkowego w konstruktorze. Przy wejściu z wartością domyślną (input(10)) błędu nie ma, ale konstruktor odczyta wartość domyślną, a nie tę od rodzica — to podstępny błąd, widać go w przykładzie 4. Dlaczego nie computed? Bo computed jest tylko do odczytu: nie dałoby się w nim pisać w polu.

Zatrzymaj się

Przycisk „Cofnij zmiany” robi roboczy.set(nick()) w szablonie. Dlaczego tu odczyt wejścia jest już bezpieczny?

PRZYKŁAD 3

ngOnChanges: poprzednia wartość wejścia

Wiersz wyniku pokazuje rangę gracza i strzałkę: o ile punktów zmienił się wynik od poprzedniego razu. Ranga zależy tylko od obecnych punktów — to computed. Strzałka potrzebuje poprzedniej wartości, której sygnał nie pamięta — tu pomaga ngOnChanges.

src/app/nowa-38-3/wynik.ts — pełny plik
import { Component, OnChanges, SimpleChanges, computed, input, signal } from '@angular/core';

@Component({
    selector: 'app-wynik',
    imports: [],
    templateUrl: './wynik.html'
})
export class Wynik implements OnChanges {
    readonly nick = input.required<string>();
    readonly punkty = input.required<number>();

    readonly ranga = computed(() => (this.punkty() >= 100 ? 'mistrz' : 'gracz'));
    readonly zmiana = signal(0);

    ngOnChanges(zmiany: SimpleChanges<Wynik>): void {
        const p = zmiany.punkty;
        if (p && !p.firstChange) {
            this.zmiana.set(p.currentValue - p.previousValue);
        }
    }
}
src/app/nowa-38-3/wynik.html — pełny plik
<div class="d-flex align-items-center gap-2">
  <strong style="width: 5rem">{{ nick() }}</strong>
  <span style="width: 4rem">{{ punkty() }} pkt</span>
  <span class="badge" [class.text-bg-warning]="ranga() === 'mistrz'" [class.text-bg-secondary]="ranga() === 'gracz'">{{ ranga() }}</span>
  @if (zmiana() > 0) {
    <span class="text-success">▲ +{{ zmiana() }}</span>
  } @else if (zmiana() < 0) {
    <span class="text-danger">▼ {{ zmiana() }}</span>
  }
</div>
src/app/nowa-38-3/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { Wynik } from './wynik';

@Component({
    selector: 'app-demo',
    imports: [Wynik],
    templateUrl: './demo.html'
})
export class Demo {
    readonly gracze = signal([
        { id: 1, nick: 'Pixel', punkty: 90 },
        { id: 2, nick: 'Bajt', punkty: 75 },
        { id: 3, nick: 'Kursor', punkty: 60 },
    ]);

    dodaj(id: number, ile: number): void {
        this.gracze.update(l => l.map(g => (g.id === id ? { ...g, punkty: g.punkty + ile } : g)));
    }
}
src/app/nowa-38-3/demo.html — pełny plik
<div class="p-3" style="max-width: 36rem">
  <ul class="list-group">
    @for (g of gracze(); track g.id) {
      <li class="list-group-item d-flex justify-content-between align-items-center">
        <app-wynik [nick]="g.nick" [punkty]="g.punkty" />
        <span>
          <button class="btn btn-sm btn-outline-success" (click)="dodaj(g.id, 15)">+15</button>
          <button class="btn btn-sm btn-outline-danger ms-1" (click)="dodaj(g.id, -10)">−10</button>
        </span>
      </li>
    }
  </ul>
</div>
po „+15” u Pixela i „−10” u Bajta
Pixel   105 pkt  [mistrz]  ▲ +15
Bajt     65 pkt  [gracz]   ▼ -10
Kursor   60 pkt  [gracz]

p.firstChange jest true przy pierwszym przekazaniu wartości — wtedy nie ma z czym porównać, więc strzałki nie pokazujemy. ngOnChanges wywołuje się tylko dla zmian zrobionych przez rodzica i tylko wtedy, gdy do wejścia trafia inna wartość — dlatego rodzic tworzy nowy obiekt gracza przez map, jak w lekcji 31.

Zatrzymaj się

Czy rangę dałoby się też liczyć w ngOnChanges? Dlaczego computed jest tu lepszy?

PRZYKŁAD 4

ngOnInit i ngOnDestroy: stoper z wejścia

Odliczanie do startu turnieju dostaje liczbę sekund wejściem. Stoper startuje w ngOnInit — dopiero wtedy wiadomo, od ilu liczyć — a zatrzymuje się w ngOnDestroy, gdy rodzic zamknie odliczanie.

src/app/nowa-38-4/odliczanie.ts — pełny plik
import { Component, OnDestroy, OnInit, input, signal } from '@angular/core';

@Component({
    selector: 'app-odliczanie',
    imports: [],
    templateUrl: './odliczanie.html'
})
export class Odliczanie implements OnInit, OnDestroy {
    readonly sekundy = input(10);
    readonly zostalo = signal(0);
    private numer = 0;

    ngOnInit(): void {
        this.zostalo.set(this.sekundy());
        this.numer = setInterval(() => {
            this.zostalo.update(s => s - 1);
            if (this.zostalo() === 0) {
                clearInterval(this.numer);
            }
        }, 1000);
    }

    ngOnDestroy(): void {
        clearInterval(this.numer);
        console.log('odliczanie przerwane, zostało', this.zostalo(), 's');
    }
}
src/app/nowa-38-4/odliczanie.html — pełny plik
<div class="alert mb-0" [class.alert-info]="zostalo() > 0" [class.alert-success]="zostalo() === 0">
  @if (zostalo() > 0) {
    Do startu turnieju: <strong>{{ zostalo() }} s</strong>
  } @else {
    <strong>Start!</strong>
  }
</div>
src/app/nowa-38-4/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { Odliczanie } from './odliczanie';

@Component({
    selector: 'app-demo',
    imports: [Odliczanie],
    templateUrl: './demo.html'
})
export class Demo {
    readonly czas = signal(0);
}
src/app/nowa-38-4/demo.html — pełny plik
<div class="p-3" style="max-width: 26rem">
  <div class="mb-3">
    <button class="btn btn-primary me-2" [disabled]="czas() > 0" (click)="czas.set(5)">Start za 5 s</button>
    <button class="btn btn-primary me-2" [disabled]="czas() > 0" (click)="czas.set(15)">Start za 15 s</button>
    <button class="btn btn-outline-danger" [disabled]="czas() === 0" (click)="czas.set(0)">Zamknij</button>
  </div>
  @if (czas() > 0) {
    <app-odliczanie [sekundy]="czas()" />
  }
</div>
„Start za 5 s”, po 2 sekundach „Zamknij”
Do startu turnieju: 3 s
(odliczanie znika)
Konsola: odliczanie przerwane, zostało 3 s

Numer stopera jest polem klasy (private numer = 0), bo potrzebują go dwie metody. Przenieś start stopera do konstruktora: kompilator nic nie powie, bo sekundy ma wartość domyślną, ale odliczanie zawsze zacznie się od 10, bez względu na przycisk — konstruktor widzi wartość domyślną, nie wartość od rodzica.

ngOnDestroy czy DestroyRef?

Robią to samo. inject(DestroyRef).onDestroy(…) z lekcji 37 pozwala zapisać sprzątanie zaraz obok kodu, który go wymaga, i działa także w serwisach. ngOnDestroy spotkasz w większości istniejącego kodu. W jednym komponencie wybierz jeden sposób.

Zatrzymaj się

Gdy odliczanie trwa, przyciski startu są zablokowane. Co by się stało, gdyby rodzic zmienił czas w trakcie odliczania? Która metoda by się wtedy wywołała, a która nie?

ELEMENTY WBUDOWANE

Zestawienie elementów

Potrzebujesz…UżyjUwagi
wartości wyliczanej z wejściacomputedprzelicza się sama przy każdej zmianie (lekcja 29)
czynności przy każdej zmianie sygnału lub wejściaeffecttytuł strony, zapis w localStorage (lekcja 37)
jednorazowego startu z wartością wejściangOnInitkopia robocza, stoper, pobranie danych dla id
poprzedniej wartości wejściangOnChanges + SimpleChangespreviousValue, currentValue, firstChange
sprzątania przy usunięciungOnDestroy albo DestroyRefto samo działanie, dwa zapisy
inject(…), effect(…)pole klasy albo constructornie w ngOnInit ani w metodach (NG0203, lekcja 33)
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Odczyt wejścia obowiązkowego w konstruktorzeNG8118: `nick` is a required `input` and does not have a value in this context. Przenieś kod do ngOnInit.
Odczyt wejścia z wartością domyślną w konstruktorzeBrak błędu, ale konstruktor widzi wartość domyślną, a nie wartość od rodzica.
Literówka ngOninit() przy implements OnInitTS2420: Class 'Karta’ incorrectly implements interface 'OnInit’. Property 'ngOnInit’ is missing …. Bez implements — brak błędu, a metoda po cichu nigdy się nie wywoła.
Oczekiwanie, że ngOnInit zadziała po zmianie wejściangOnInit wywołuje się raz. Na późniejsze zmiany reaguj computed, effect albo ngOnChanges.
Wejście dostaje cały obiekt ([gracz]="g"), a rodzic zmienia jego pole: g.punkty += 15ngOnChanges się nie wywołuje — do wejścia trafia ten sam obiekt. Twórz nowe obiekty (map i ...).
Logika wyliczania w ngOnChanges zamiast w computedDziała, ale wymaga ręcznego ustawiania sygnału i łatwo o pominięty przypadek. Do wartości wyliczanych — computed.
ZADANIA

Zadania

ZAD 1Dziennik z drugim wejściem★☆☆

Dodaj do karty z przykładu 1 drugie wejście sala i przycisk u rodzica, który zmienia salę. W ngOnChanges zapisuj do dziennika zmianę każdego z wejść osobno.

Kryterium sukcesu: zmiana tytułu daje wpis tylko o tytule, zmiana sali — tylko o sali; przy pierwszym pokazaniu są oba wpisy.

Podpowiedź

Dwa osobne if: if (zmiany.tytul) { … } i if (zmiany.sala) { … }.

ZAD 2Napraw konstruktor★☆☆

Komponent app-powitanie z wejściem imie (domyślnie 'gościu') w konstruktorze ustawia sygnał tekst na „Cześć, imie!”. Rodzic przekazuje imie="Ala", a na ekranie i tak jest „Cześć, gościu!”. Znajdź przyczynę i napraw na dwa sposoby: przez ngOnInit i przez computed.

Kryterium sukcesu: obie wersje pokazują „Cześć, Ala!”; w komentarzu w kodzie jest wyjaśnienie, która wersja reaguje też na późniejsze zmiany imienia u rodzica.

Podpowiedź

computed(() => 'Cześć, ' + this.imie() + '!') nie potrzebuje żadnej metody cyklu życia.

ZAD 3Edycja wydarzenia★★☆

Komponent app-edycja-wydarzenia dostaje obiekt wydarzenia (tytuł, sala, limit) przez input.required. W ngOnInit kopiuje pola do trzech roboczych sygnałów połączonych z polami formularza. „Zapisz” wysyła zdarzenie z nowym obiektem, „Anuluj” — zdarzenie bez danych. Rodzic ma listę trzech wydarzeń i edytuje jedno naraz.

Kryterium sukcesu: zmiany w formularzu nie są widoczne na liście, dopóki nie klikniesz „Zapisz”; „Anuluj” zostawia listę bez zmian.

Podpowiedź

Przykład 2 z trzema polami. Nowy obiekt: { ...this.wydarzenie(), tytul: this.tytul(), … }.

ZAD 4Kurs waluty ze strzałką★★☆

Komponent app-kurs z wejściami waluta i kurs (liczba). Pokazuje kurs z dwoma miejscami po przecinku, strzałkę ▲/▼ i procent zmiany względem poprzedniej wartości. Rodzic ma przycisk „Losuj kursy”, który zmienia kursy trzech walut o losową wartość od −2% do +2%.

Kryterium sukcesu: strzałka i procent zgadzają się z poprzednią wartością; przy pierwszym wyświetleniu nie ma strzałki.

Podpowiedź

Przykład 3. Procent: (nowy - stary) / stary * 100. Losowanie: Math.random() * 0.04 - 0.02.

ZAD 5Quiz na czas★★★

Komponent app-pytanie z wejściami tresc i czas (sekundy). W ngOnInit uruchamia odliczanie, po zerze wysyła zdarzenie koniecCzasu; kliknięcie odpowiedzi wysyła odpowiedziano. W ngOnDestroy zatrzymuje stoper. Rodzic pokazuje pytania po kolei — zawsze tylko jedno.

Kryterium sukcesu: każde pytanie ma własne odliczanie od pełnego czasu; przejście do następnego pytania zatrzymuje poprzedni stoper (sprawdź wpisem w konsoli).

Podpowiedź

Żeby każde pytanie było nowym komponentem, pokaż je w @for z jednym elementem: @for (p of [biezace()]; track p.id) — zmiana id usuwa stary komponent i tworzy nowy.

ZAD 6Ta sama karta, trzy wersje★★★

Komponent wyświetlający „Wolnych miejsc: X z Y” i pasek zapełnienia na podstawie wejść limit i zapisanych napisz trzy razy: (a) z computed, (b) z ngOnChanges ustawiającym sygnały, (c) z ngOnInit. Rodzic pokazuje wszystkie trzy obok siebie i ma przyciski „+1 zapis” i „−1 zapis”.

Kryterium sukcesu: wersje (a) i (b) zmieniają się po kliknięciach, wersja (c) nie; w komentarzu w kodzie każdej wersji jest jedno zdanie, dlaczego tak się zachowuje.

Podpowiedź

To ćwiczenie porównawcze — wersja (c) ma „nie działać”. Policz też linijki kodu każdej wersji.

PODSUMOWANIE

Co trzeba zapamiętać

  • Kolejność: constructor → ngOnChanges → ngOnInit → (zmiany wejść: ngOnChanges) → ngOnDestroy.
  • W konstruktorze wejścia nie mają wartości od rodzica: wejście obowiązkowe daje NG8118, a wejście z wartością domyślną — po cichu złą wartość.
  • ngOnInit — jednorazowy start z wartościami wejść; ngOnChanges — reakcja na zmiany z dostępem do poprzedniej wartości.
  • ngOnDestroy i DestroyRef.onDestroy robią to samo — sprzątają przy usunięciu komponentu.
  • Wartości wyliczane z wejść liczysz w computed; metody cyklu życia zostaw na to, czego sygnały nie potrafią.

Sprawdź się: komponent ma pokazać szczegóły ucznia o id z wejścia i pobrać je z pliku JSON. W której metodzie wyślesz zapytanie i dlaczego nie w konstruktorze?

Dokumentacja: angular.dev — Component lifecycle.

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