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

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

Angular 22 @for track $index $first $even @empty interface table 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyświetlisz każdy element tablicy blokiem @for z obowiązkowym track.
  • 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, $even i $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.

TEORIA

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.

składnia
@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&lt;Wydarzenie[]&gt;([...])</code> — kompilator wyłapie literówkę w nazwie pola także w szablonie.</p>
PRZYKŁAD 1

Lista tekstów

Najprostsza lista: tablica napisów wyświetlona jako list-group Bootstrapa.

src/app/nowa-13-1/demo.ts — pełny plik
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']);
}
src/app/nowa-13-1/demo.html — pełny plik
<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>
wynik w przeglądarce
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.

PRZYKŁAD 2

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.

src/app/nowa-13-2/demo.ts — pełny plik
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 },
    ]);
}
src/app/nowa-13-2/demo.html — pełny plik
<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>
wynik w przeglądarce
[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?

PRZYKŁAD 3

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.

src/app/nowa-13-3/demo.ts — pełny plik
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 },
    ]);
}
src/app/nowa-13-3/demo.html — pełny plik
<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>
wynik w przeglądarce
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.

PRZYKŁAD 4

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.

src/app/nowa-13-4/demo.ts — pełny plik
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));
    }
}
src/app/nowa-13-4/demo.html — pełny plik
<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>
wynik po usunięciu wszystkich zadań
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?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
@for (x of lista(); track x.id) { … }fragment dla każdego elementuZawsze of, zawsze track.
@empty { … }treść dla pustej tablicyZaraz po bloku @for.
$index, $first, $last, $even, $odd, $countzmienne pomocnicze pętliTylko wewnątrz bloku.
let nr = $indexwłasna nazwa zmiennejPo średniku za track.
interface Nazwa { … }kształt obiektuZ TypeScriptu.
signal<Wydarzenie[]>([…])sygnał z tablicą obiektówKompilator sprawdza pola.
table, table-lighttabela Bootstrapa, jasny wierszTeż table-striped.
h-100wysokość 100% rodzicaRówne karty w rzędzie.
d-flex justify-content-betweenelementy na dwóch końcachBootstrap.
*ngFor="let x of lista"starsza składniaImport NgFor.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
@for (w of wydarzenia()) bez trackNG5002: @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 miejscaNumeracja od 0. Dla ludzi pisz {{ $index + 1 }}.
{{ $count }} poza pętląTS2339 — zmienne pomocnicze istnieją tylko w bloku @for.
this.lista().push(…) przy dodawaniuPułapka z lekcji 10 — twórz nową tablicę.
ZADANIA

Zadania

ZAD 1Plan lekcji na dziś★☆☆

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

ZAD 2Menu stołówki★☆☆

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.

ZAD 3Dodaj i usuń★★☆

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.

ZAD 4Galeria plakatów★★☆

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.

ZAD 5Ranking z awansem★★★

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.

ZAD 6Tabliczka mnożenia★★★

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

PODSUMOWANIE

Co trzeba zapamiętać

  • @for (x of lista(); track x.id) { … } powtarza fragment dla każdego elementu; track jest obowiązkowy.
  • W track wybierasz unikalne pole (zwykle id), a dla listy różnych tekstów — sam element.
  • $index, $first, $last, $even, $odd, $count działają tylko w pętli.
  • @empty pokazuje 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.

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