@for, track, @empty — listy z tablic
Lista wydarzeń, tabela wyników, galeria zdjęć — wszędzie ten sam kawałek HTML powtarza się dla każdego elementu tablicy. Zamiast kopiować go ręcznie, napiszesz go raz w bloku @for, a liczba kopii na ekranie będzie się zmieniać razem z tablicą.
Czego się dziś nauczysz
- Wyświetlisz każdy element tablicy blokiem
@forz obowiązkowymtrack. - Dobierzesz wyrażenie do
track: unikalne pole obiektu albo sam element. - Opiszesz kształt obiektów interfejsem i zbudujesz z nich karty w siatce.
- Użyjesz zmiennych
$index,$first,$last,$eveni$count. - Pokażesz komunikat dla pustej listy blokiem
@empty. - Usuniesz element z listy przyciskiem w wierszu, przekazując jego
id. - Zbudujesz tabelę Bootstrapa z danych.
- Rozpoznasz starszą składnię
*ngFor.
Przygotowanie: lekcje 10–12 — umiesz dodawać i usuwać elementy tablicy w sygnale i używać @if. Przewidywany czas: 2 × 45 min z zadaniami.
Ten sam HTML, wiele razy
Masz tablicę pięciu wydarzeń i chcesz pokazać każde jako kartę. Możesz skopiować kartę pięć razy i w każdej wpisać wydarzenia()[0], wydarzenia()[1]… Ale co, jeśli jutro wydarzeń będzie siedem? Albo zero?
Blok @for działa jak for…of w TypeScripcie, tylko powtarza fragment szablonu. Ile elementów w tablicy, tyle kopii na ekranie — i liczba ta zmienia się sama, gdy zmienia się tablica w sygnale.
@for (w of wydarzenia(); track w.id) {
<p>{{ w.tytul }}</p> <!-- w = bieżący element; nazwę wybierasz sam -->
} @empty {
<p>Brak wydarzeń.</p> <!-- gdy tablica jest pusta -->
}
@END
</section>
<section class="sd-section" id="track">
<span class="sd-badge sd-b-blue">TEORIA</span>
<h2>track i zmienne pomocnicze</h2>
<p><strong><code>track</code> jest obowiązkowy.</strong> Mówi Angularowi, po czym rozpoznaje element: „to wciąż ta sama karta, tylko przesunięta”. Dzięki temu po usunięciu jednego elementu Angular nie przerysowuje całej listy. Dla obiektów wybierz pole unikalne — najczęściej <code>id</code>. Dla listy tekstów bez powtórzeń wystarczy sam element: <code>track sala</code>.</p>
<div class="sd-scroll">
<table class="sd-table">
<tr><th style="width:22%">Zmienna</th><th>Znaczenie</th></tr>
<tr><td><code>$index</code></td><td>numer elementu od 0</td></tr>
<tr><td><code>$first</code>, <code>$last</code></td><td>czy to pierwszy / ostatni element (true/false)</td></tr>
<tr><td><code>$even</code>, <code>$odd</code></td><td>czy numer jest parzysty / nieparzysty</td></tr>
<tr><td><code>$count</code></td><td>liczba elementów tablicy</td></tr>
</table>
</div>
<p>Zmienne działają tylko wewnątrz bloku. Możesz używać ich wprost (<code>{{ $index + 1 }}</code>) albo nadać im własne nazwy: <code>track g.nick; let nr = $index</code> — przydaje się, gdy jedna pętla stoi w drugiej.</p>
@@BOX info | Interfejs — kształt obiektu
<p>Gdy w tablicy są obiekty, opisujemy ich kształt interfejsem (znasz go z TypeScriptu): <code>interface Wydarzenie { id: number; tytul: string; … }</code>. Sygnał zapisujemy wtedy jako <code>signal<Wydarzenie[]>([...])</code> — kompilator wyłapie literówkę w nazwie pola także w szablonie.</p>
Lista tekstów
Najprostsza lista: tablica napisów wyświetlona jako list-group Bootstrapa.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly sale = signal(['Sala 12 — informatyczna', 'Sala 7 — robotyka', 'Aula', 'Biblioteka']);
}
<div class="p-3">
<h5>Gdzie odbywają się zajęcia koła</h5>
<ul class="list-group" style="width: 22rem">
@for (sala of sale(); track sala) {
<li class="list-group-item">{{ sala }}</li>
}
</ul>
</div>
Gdzie odbywają się zajęcia koła
Sala 12 — informatyczna
Sala 7 — robotyka
Aula
Biblioteka
Dopisz w demo.ts piąty napis i zapisz plik — wiersz pojawi się bez żadnej zmiany w szablonie.
Zatrzymaj się
Dopisz drugi raz 'Aula'. Lista się wyświetli, ale zajrzyj do konsoli przeglądarki — Angular ostrzega, że track nie jest już unikalny.
Karty z tablicy obiektów
Prawdziwe dane to zwykle obiekty. Interfejs opisuje ich kształt, a karty układa siatka Bootstrapa z lekcji 03. Wewnątrz pętli działa @if — każda karta sama decyduje, którą etykietę pokazać. h-100 wyrównuje wysokość kart w rzędzie.
import { Component, signal } from '@angular/core';
interface Wydarzenie {
id: number;
tytul: string;
sala: string;
miejsca: number;
}
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly wydarzenia = signal<Wydarzenie[]>([
{ id: 1, tytul: 'Turniej retro', sala: '12', miejsca: 16 },
{ id: 2, tytul: 'Noc robotów', sala: '7', miejsca: 0 },
{ id: 3, tytul: 'Warsztat druku 3D', sala: '14', miejsca: 3 },
]);
}
<div class="p-3">
<div class="row g-3" style="max-width: 60rem">
@for (w of wydarzenia(); track w.id) {
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ w.tytul }}</h5>
<p class="card-text">Sala {{ w.sala }}</p>
@if (w.miejsca > 0) {
<span class="badge text-bg-success">Wolnych miejsc: {{ w.miejsca }}</span>
} @else {
<span class="badge text-bg-danger">Brak miejsc</span>
}
</div>
</div>
</div>
}
</div>
</div>
[Turniej retro ] [Noc robotów ] [Warsztat druku 3D ]
[Sala 12 ] [Sala 7 ] [Sala 14 ]
[Wolnych miejsc: 16 ] [Brak miejsc ] [Wolnych miejsc: 3 ]
Na wąskim ekranie karty stają jedna pod drugą (col-md-4).
Zatrzymaj się
Wewnątrz pętli piszemy w.tytul, a nie w().tytul. Dlaczego tu nie ma nawiasów, skoro wydarzenia jest sygnałem?
Numeracja i wyróżnienia: $index i spółka
Ranking turnieju w tabeli Bootstrapa (table). Numer miejsca liczymy z $index, zwycięzcę wyróżniamy przez $first, co drugi wiersz cieniujemy przez $even i wiązanie [class.table-light] z lekcji 06.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly gracze = signal([
{ nick: 'Pixel', punkty: 980 },
{ nick: 'Bajt', punkty: 870 },
{ nick: 'Kursor', punkty: 815 },
{ nick: 'Ping', punkty: 640 },
]);
}
<div class="p-3">
<table class="table" style="width: 28rem">
<thead>
<tr><th>Miejsce</th><th>Gracz</th><th>Punkty</th></tr>
</thead>
<tbody>
@for (g of gracze(); track g.nick; let nr = $index, pierwszy = $first, parzysty = $even) {
<tr [class.table-light]="parzysty">
<td>{{ nr + 1 }}.</td>
<td>
{{ g.nick }}
@if (pierwszy) { <span class="badge text-bg-warning">zwycięzca</span> }
</td>
<td>{{ g.punkty }}</td>
</tr>
}
</tbody>
</table>
<p>Graczy w rankingu: {{ gracze().length }}</p>
</div>
Miejsce Gracz Punkty
1. Pixel [zwycięzca] 980
2. Bajt 870
3. Kursor 815
4. Ping 640
Graczy w rankingu: 4
Miejsca są numerowane od 1, bo do $index dodajemy jeden.
Zatrzymaj się
Dlaczego liczba graczy pod tabelą jest liczona przez gracze().length, a nie przez $count? Spróbuj użyć $count poza blokiem i przeczytaj komunikat.
Usuwanie z listy i @empty
Każdy wiersz ma przycisk Usuń. Metoda dostaje id elementu i tworzy nową tablicę bez niego (filter z lekcji 10). Gdy usuniesz wszystko, pojawi się blok @empty. Klasy d-flex justify-content-between align-items-center rozsuwają tekst i przycisk na dwa końce wiersza.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly zadania = signal([
{ id: 1, tekst: 'Podłączyć konsole' },
{ id: 2, tekst: 'Wydrukować dyplomy' },
{ id: 3, tekst: 'Kupić napoje' },
]);
usun(id: number): void {
this.zadania.update(lista => lista.filter(z => z.id !== id));
}
}
<div class="p-3">
<h5>Do zrobienia przed turniejem</h5>
<ul class="list-group" style="width: 26rem">
@for (z of zadania(); track z.id) {
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ z.tekst }}
<button class="btn btn-sm btn-outline-danger" (click)="usun(z.id)">Usuń</button>
</li>
} @empty {
<li class="list-group-item text-success">Wszystko zrobione! 🎉</li>
}
</ul>
</div>
Do zrobienia przed turniejem
Wszystko zrobione! 🎉
Starsza składnia: *ngFor
W starszym kodzie i części arkuszy zobaczysz <li *ngFor="let z of zadania; let i = index">. Działa tak samo, wymaga importu NgFor z @angular/common, a track nie jest w niej obowiązkowy. Nie ma też odpowiednika @empty — trzeba było dopisać osobny *ngIf.
Zatrzymaj się
Przycisk przekazuje do metody z.id, a nie $index. Co mogłoby pójść źle, gdyby usuwać po numerze pozycji, a lista zmieniała się w międzyczasie?
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
@for (x of lista(); track x.id) { … } | fragment dla każdego elementu | Zawsze of, zawsze track. |
@empty { … } | treść dla pustej tablicy | Zaraz po bloku @for. |
$index, $first, $last, $even, $odd, $count | zmienne pomocnicze pętli | Tylko wewnątrz bloku. |
let nr = $index | własna nazwa zmiennej | Po średniku za track. |
interface Nazwa { … } | kształt obiektu | Z TypeScriptu. |
signal<Wydarzenie[]>([…]) | sygnał z tablicą obiektów | Kompilator sprawdza pola. |
table, table-light | tabela Bootstrapa, jasny wiersz | Też table-striped. |
h-100 | wysokość 100% rodzica | Równe karty w rzędzie. |
d-flex justify-content-between | elementy na dwóch końcach | Bootstrap. |
*ngFor="let x of lista" | starsza składnia | Import NgFor. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
@for (w of wydarzenia()) bez track | NG5002: @for loop must have a „track” expression. |
@for (w in wydarzenia(); …) | NG5002: must match the pattern „<identifier> of <expression>”. Zawsze of. |
@for (w of wydarzenia; track w.id) | TS2488 — sygnał to nie tablica. Dopisz nawiasy: wydarzenia(). |
track w.tytul, gdy tytuły się powtarzają | Lista się wyświetla, ale konsola przeglądarki ostrzega o zduplikowanych kluczach. Wybierz pole unikalne. |
{{ $index }} jako numer miejsca | Numeracja od 0. Dla ludzi pisz {{ $index + 1 }}. |
{{ $count }} poza pętlą | TS2339 — zmienne pomocnicze istnieją tylko w bloku @for. |
this.lista().push(…) przy dodawaniu | Pułapka z lekcji 10 — twórz nową tablicę. |
Zadania
Wyświetl tablicę sześciu przedmiotów jako listę numerowaną od 1 (w formacie „1. Matematyka”). Ostatni przedmiot zaznacz pogrubieniem.
Kryterium sukcesu: wszystkie przedmioty są na ekranie, ostatni jest pogrubiony, a dopisanie siódmego w kodzie nie wymaga zmian w szablonie.
Podpowiedź
{{ $index + 1 }}. oraz [class.fw-bold]="$last".
Zrób tablicę obiektów { id, danie, cena } z pięcioma daniami (z interfejsem) i pokaż je w tabeli Bootstrapa z kolumnami „Danie” i „Cena”. Ceny z dwoma miejscami po kropce, co drugi wiersz cieniowany.
Kryterium sukcesu: tabela ma nagłówek i pięć wierszy, ceny mają format 12.50, wiersze są na zmianę jasne i białe.
Podpowiedź
{{ d.cena.toFixed(2) }} i [class.table-light]="$even" albo klasa table-striped na całej tabeli.
Rozbuduj przykład 4: pole #pole i przycisk „Dodaj zadanie”. Każde nowe zadanie dostaje kolejny numer id. Pod listą pokaż „Zostało zadań: X”.
Kryterium sukcesu: dodawanie i usuwanie działa w dowolnej kolejności, liczba pod listą jest zawsze poprawna, a id nigdy się nie powtarza.
Podpowiedź
Trzymaj w klasie zwykłe pole nastepneId = 4 i zwiększaj je po każdym dodaniu. Licznik zadań to dobre miejsce na computed.
Tablica obiektów { id, tytul, plakat } z adresami obrazków z lekcji 07. Pokaż każdy element jako kartę z obrazkiem u góry, w siatce po trzy w rzędzie. Pod siatką przyciski „Usuń ostatni” i „Przywróć wszystkie”. Gdy tablica jest pusta, pokaż szary napis „Galeria jest pusta”.
Kryterium sukcesu: karty mają obrazki i tytuły, usuwanie działa do zera, a przy pustej tablicy widać komunikat z @empty.
Podpowiedź
[src]="w.plakat". „Przywróć” ustawia kopię początkowej tablicy przechowywanej w zwykłym polu readonly.
Rozbuduj przykład 3: przy każdym graczu przycisk „+50 pkt”. Ranking ma się sam sortować od najwyższego wyniku, pierwsze trzy miejsca mają mieć złotą, srebrną i brązową etykietę („1.”, „2.”, „3.”), a pozostali — zwykły numer.
Kryterium sukcesu: po dodaniu punktów gracz przesuwa się w tabeli na właściwe miejsce, a etykiety podium są zawsze przy trzech najlepszych.
Podpowiedź
Posortowaną listę zrób w computed: [...this.gracze()].sort((a, b) => b.punkty - a.punkty) — kopia, bo sort zmienia tablicę. Punkty dodaj przez map z kopią obiektu (lekcja 10). Kolory podium: @if ($index === 0) itd.
Wyświetl tabliczkę mnożenia 10×10 jako tabelę Bootstrapa, korzystając z dwóch zagnieżdżonych bloków @for na tablicy [1, 2, …, 10]. Pierwszy wiersz i pierwsza kolumna to nagłówki (pogrubione), a przekątna (3×3, 7×7…) ma inne tło.
Kryterium sukcesu: tabela ma 10 wierszy i 10 kolumn z poprawnymi wynikami; komórki na przekątnej są wyróżnione.
Podpowiedź
Tablicę liczb zrób w klasie: Array.from({ length: 10 }, (_, i) => i + 1). W pętli zewnętrznej nazwij liczbę w, w wewnętrznej k, i porównaj w === k w [class.table-warning].
Co trzeba zapamiętać
@for (x of lista(); track x.id) { … }powtarza fragment dla każdego elementu;trackjest obowiązkowy.- W
trackwybierasz unikalne pole (zwykleid), a dla listy różnych tekstów — sam element. $index,$first,$last,$even,$odd,$countdziałają tylko w pętli.@emptypokazuje treść zastępczą dla pustej tablicy.- Element w pętli to zwykły obiekt — bez nawiasów:
w.tytul. - Usuwasz po
id, nie po numerze pozycji.
Sprawdź się: masz listę uczniów, w której mogą się powtórzyć imiona i nazwiska. Co wybierzesz do track i co dopiszesz do danych, jeśli nic unikalnego jeszcze nie ma?
Dokumentacja: angular.dev — blok @for.