Dyrektywy pętli w Angularze – @for i *ngFor

1. Czym są dyrektywy pętli?

Dyrektywy pętli to narzędzia w Angularze, które pozwalają na automatyczne tworzenie wielu elementów na podstawie listy danych. Dzięki nim możemy wyświetlić wszystkie elementy z tablicy bez ręcznego kopiowania kodu HTML.

Przykłady zastosowań:

  • Lista filmów w bazie danych
  • Menu nawigacyjne z różnymi opcjami
  • Tabela z danymi użytkowników
  • Galeria zdjęć
  • Lista komentarzy pod artykułem

Bez pętli musielibyśmy:

<!-- Bez pętli - dużo powtarzającego się kodu! -->
<div>Film 1: Avengers</div>
<div>Film 2: Spider-Man</div>
<div>Film 3: Batman</div>
<!-- ... i tak dalej dla każdego filmu -->

Z pętlą:

<!-- Z pętlą - kod napisany raz działa dla wszystkich filmów! -->
@for (film of filmy; track film.id) {
  <div>{{ film.title }}</div>
}

2. Nowoczesna składnia @for (Angular 17+)

Podobnie jak z @if, Angular wprowadził nową, lepszą składnię@for. Jest ona:

  • Bardziej czytelna – przypomina pętle for z innych języków programowania
  • Nie wymaga importów – nie trzeba dodawać żadnych modułów
  • Wydajniejsza – Angular działa szybciej
  • Bezpieczniejsza – wymaga track co poprawia performance

Podstawowa składnia @for

@for (element of tablica; track element.id) {
  <div>{{ element.nazwa }}</div>
}

Gdzie:

  • element – nazwa zmiennej dla każdego elementu (możesz nazwać jak chcesz)
  • tablica – nazwa tablicy z komponentu
  • track element.id – mówi Angularowi jak identyfikować elementy (WYMAGANE!)

3. Przykłady z @for – od prostych do bardziej skomplikowanych

Przykład 1: Lista filmów – podstawy

// Komponent TypeScript
import { Component } from '@angular/core';

@Component({
  selector: 'app-film-list',
  standalone: true,
  imports: [], // Nie potrzebujemy żadnych importów dla @for!
  templateUrl: './film-list.component.html',
  styleUrl: './film-list.component.css'
})
export class FilmListComponent {
  filmy = [
    'Avengers: Endgame',
    'Spider-Man',
    'Batman',
    'Superman',
    'Iron Man'
  ];
}
<!-- Szablon HTML -->
<div class="container">
  <h2>Lista filmów</h2>
  
  <div class="row">
    @for (film of filmy; track $index) {
      <div class="col-md-4 mb-3">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title">{{ film }}</h5>
            <p class="text-muted">Film numer: {{ $index + 1 }}</p>
          </div>
        </div>
      </div>
    }
  </div>
</div>

Wyjaśnienie:

  • $index – automatyczna zmienna z numerem elementu (zaczyna od 0)
  • track $index – używamy gdy nie mamy unikalnego ID
  • $index + 1 – dodajemy 1, bo użytkownicy liczą od 1, nie od 0

Przykład 2: Obiekty z wieloma właściwościami

export class FilmCatalogComponent {
  filmy = [
    {
      id: 1,
      title: 'Avengers: Endgame',
      year: 2019,
      genre: 'Akcja',
      rating: 8.4,
      description: 'Finałowa walka z Thanosem'
    },
    {
      id: 2,
      title: 'Spider-Man: No Way Home',
      year: 2021,
      genre: 'Akcja',
      rating: 8.2,
      description: 'Multiwersum Spider-Mana'
    },
    {
      id: 3,
      title: 'The Dark Knight',
      year: 2008,
      genre: 'Akcja',
      rating: 9.0,
      description: 'Batman kontra Joker'
    },
    {
      id: 4,
      title: 'Inception',
      year: 2010,
      genre: 'Sci-Fi',
      rating: 8.8,
      description: 'Świat snów i rzeczywistości'
    }
  ];
}
<div class="container">
  <h2>Katalog filmów</h2>
  
  <div class="row">
    @for (film of filmy; track film.id) {
      <div class="col-lg-6 mb-4">
        <div class="card h-100">
          <div class="card-header d-flex justify-content-between">
            <h5 class="mb-0">{{ film.title }}</h5>
            <span class="badge bg-primary">{{ film.year }}</span>
          </div>
          <div class="card-body">
            <p class="card-text">{{ film.description }}</p>
            <div class="mb-2">
              <strong>Gatunek:</strong> {{ film.genre }}
            </div>
            <div class="mb-2">
              <strong>Ocena:</strong> 
              <span class="badge bg-warning text-dark">{{ film.rating }}/10</span>
            </div>
          </div>
          <div class="card-footer">
            <button class="btn btn-primary btn-sm">Zobacz szczegóły</button>
            <button class="btn btn-outline-secondary btn-sm ms-2">Dodaj do ulubionych</button>
          </div>
        </div>
      </div>
    }
  </div>
</div>

Przykład 3: @for z @if – warunkowe wyświetlanie

export class FilmFilterComponent {
  filmy = [
    { id: 1, title: 'Avengers', rating: 8.4, isPopular: true },
    { id: 2, title: 'Spider-Man', rating: 8.2, isPopular: true },
    { id: 3, title: 'Low Budget Movie', rating: 5.1, isPopular: false },
    { id: 4, title: 'The Dark Knight', rating: 9.0, isPopular: true },
    { id: 5, title: 'Bad Movie', rating: 3.2, isPopular: false }
  ];
  
  showOnlyPopular = false;
  
  toggleFilter() {
    this.showOnlyPopular = !this.showOnlyPopular;
  }
}
<div class="container">
  <h2>Filmy z filtrowaniem</h2>
  
  <div class="mb-3">
    <button (click)="toggleFilter()" class="btn btn-outline-primary">
      @if (showOnlyPopular) {
        <span>Pokaż wszystkie filmy</span>
      } @else {
        <span>Pokaż tylko popularne</span>
      }
    </button>
  </div>
  
  <div class="row">
    @for (film of filmy; track film.id) {
      <!-- Pokazuj film tylko jeśli spełnia warunki -->
      @if (!showOnlyPopular || film.isPopular) {
        <div class="col-md-6 mb-3">
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">
                {{ film.title }}
                @if (film.isPopular) {
                  <span class="badge bg-success ms-2">Popularne</span>
                }
              </h5>
              <p>Ocena: {{ film.rating }}/10</p>
              
              @if (film.rating >= 8.0) {
                <div class="alert alert-success p-2">
                  🏆 Doskonały film!
                </div>
              } @else if (film.rating >= 6.0) {
                <div class="alert alert-info p-2">
                  👍 Dobry film
                </div>
              } @else {
                <div class="alert alert-warning p-2">
                  👎 Słaby film
                </div>
              }
            </div>
          </div>
        </div>
      }
    }
  </div>
</div>

Przykład 4: Dynamiczne dodawanie i usuwanie elementów

export class FilmManagerComponent {
  filmy = [
    { id: 1, title: 'Avengers', year: 2019 },
    { id: 2, title: 'Spider-Man', year: 2021 }
  ];
  
  newFilmTitle = '';
  newFilmYear = '';
  nextId = 3;
  
  addFilm() {
    if (this.newFilmTitle.length > 0 && this.newFilmYear.length > 0) {
      this.filmy.push({
        id: this.nextId++,
        title: this.newFilmTitle,
        year: parseInt(this.newFilmYear)
      });
      
      // Wyczyść formularz
      this.newFilmTitle = '';
      this.newFilmYear = '';
    }
  }
  
  removeFilm(id: number) {
    this.filmy = this.filmy.filter(film => film.id !== id);
  }
  
  clearAll() {
    this.filmy = [];
  }
}
<div class="container">
  <h2>Zarządzanie filmami</h2>
  
  <!-- Formularz dodawania -->
  <div class="card mb-4">
    <div class="card-header">
      <h5>Dodaj nowy film</h5>
    </div>
    <div class="card-body">
      <div class="row">
        <div class="col-md-6 mb-3">
          <label class="form-label">Tytuł filmu:</label>
          <input 
            type="text" 
            #titleInput
            (input)="newFilmTitle = titleInput.value"
            value="{{ newFilmTitle }}"
            class="form-control"
            placeholder="Wpisz tytuł filmu">
        </div>
        <div class="col-md-4 mb-3">
          <label class="form-label">Rok produkcji:</label>
          <input 
            type="number" 
            #yearInput
            (input)="newFilmYear = yearInput.value"
            value="{{ newFilmYear }}"
            class="form-control"
            placeholder="2024">
        </div>
        <div class="col-md-2 mb-3 d-flex align-items-end">
          <button (click)="addFilm()" class="btn btn-success w-100">
            Dodaj film
          </button>
        </div>
      </div>
    </div>
  </div>
  
  <!-- Lista filmów -->
  <div class="d-flex justify-content-between align-items-center mb-3">
    <h5>Lista filmów ({{ filmy.length }} filmów)</h5>
    @if (filmy.length > 0) {
      <button (click)="clearAll()" class="btn btn-danger btn-sm">
        Usuń wszystkie
      </button>
    }
  </div>
  
  @if (filmy.length === 0) {
    <div class="alert alert-info">
      <h5>📽️ Brak filmów</h5>
      <p>Dodaj pierwszy film używając formularza powyżej!</p>
    </div>
  } @else {
    <div class="row">
      @for (film of filmy; track film.id) {
        <div class="col-md-6 col-lg-4 mb-3">
          <div class="card">
            <div class="card-body">
              <h6 class="card-title">{{ film.title }}</h6>
              <p class="card-text text-muted">Rok: {{ film.year }}</p>
              <button 
                (click)="removeFilm(film.id)" 
                class="btn btn-outline-danger btn-sm">
                🗑️ Usuń
              </button>
            </div>
          </div>
        </div>
      }
    </div>
  }
</div>

Przykład 5: Zagnieżdżone pętle – kategorie filmów

export class FilmCategoriesComponent {
  kategorie = [
    {
      id: 1,
      name: 'Akcja',
      filmy: [
        { id: 1, title: 'Avengers', rating: 8.4 },
        { id: 2, title: 'John Wick', rating: 7.8 },
        { id: 3, title: 'Mad Max', rating: 8.1 }
      ]
    },
    {
      id: 2,
      name: 'Komedia',
      filmy: [
        { id: 4, title: 'Deadpool', rating: 8.0 },
        { id: 5, title: 'Guardians of Galaxy', rating: 8.0 }
      ]
    },
    {
      id: 3,
      name: 'Horror',
      filmy: [
        { id: 6, title: 'IT', rating: 7.3 },
        { id: 7, title: 'A Quiet Place', rating: 7.5 },
        { id: 8, title: 'Hereditary', rating: 7.3 }
      ]
    }
  ];
}
<div class="container">
  <h2>Filmy według kategorii</h2>
  
  @for (kategoria of kategorie; track kategoria.id) {
    <div class="card mb-4">
      <div class="card-header">
        <h4 class="mb-0">
          🎬 {{ kategoria.name }} 
          <span class="badge bg-secondary ms-2">{{ kategoria.filmy.length }} filmów</span>
        </h4>
      </div>
      <div class="card-body">
        <div class="row">
          @for (film of kategoria.filmy; track film.id) {
            <div class="col-md-4 mb-3">
              <div class="card border-light">
                <div class="card-body p-3">
                  <h6 class="card-title">{{ film.title }}</h6>
                  <div class="d-flex justify-content-between align-items-center">
                    <span class="badge bg-warning text-dark">{{ film.rating }}/10</span>
                    <button class="btn btn-outline-primary btn-sm">Szczegóły</button>
                  </div>
                </div>
              </div>
            </div>
          }
        </div>
      </div>
    </div>
  }
</div>

4. Ważne zmienne w @for

Angular udostępnia specjalne zmienne, które możesz używać wewnątrz pętli:

@for (film of filmy; track film.id) {
  <div class="card mb-2">
    <div class="card-body">
      <h5>{{ film.title }}</h5>
      <small class="text-muted">
        Pozycja: {{ $index + 1 }} z {{ $count }} | 
        @if ($first) { <span class="badge bg-success">Pierwszy</span> }
        @if ($last) { <span class="badge bg-danger">Ostatni</span> }
        @if ($even) { <span class="badge bg-info">Parzysta pozycja</span> }
        @if ($odd) { <span class="badge bg-warning">Nieparzysta pozycja</span> }
      </small>
    </div>
  </div>
}

Dostępne zmienne:

  • $index – numer elementu (zaczyna od 0)
  • $count – całkowita liczba elementów
  • $firsttrue dla pierwszego elementu
  • $lasttrue dla ostatniego elementu
  • $eventrue dla elementów o parzystym indeksie (0, 2, 4…)
  • $oddtrue dla elementów o nieparzystym indeksie (1, 3, 5…)

5. @for z @empty – obsługa pustych list

export class EmptyListComponent {
  filmy: any[] = []; // Pusta tablica
  
  loadMovies() {
    this.filmy = [
      { id: 1, title: 'Avengers', rating: 8.4 },
      { id: 2, title: 'Spider-Man', rating: 8.2 }
    ];
  }
  
  clearMovies() {
    this.filmy = [];
  }
}
<div class="container">
  <h2>Lista z obsługą pustej tablicy</h2>
  
  <div class="mb-3">
    <button (click)="loadMovies()" class="btn btn-primary me-2">
      Załaduj filmy
    </button>
    <button (click)="clearMovies()" class="btn btn-secondary">
      Wyczyść listę
    </button>
  </div>
  
  @for (film of filmy; track film.id) {
    <div class="card mb-2">
      <div class="card-body">
        <h5>{{ film.title }}</h5>
        <p>Ocena: {{ film.rating }}/10</p>
      </div>
    </div>
  } @empty {
    <div class="alert alert-info">
      <h5>📭 Brak filmów na liście</h5>
      <p>Kliknij "Załaduj filmy", aby dodać filmy do listy.</p>
    </div>
  }
</div>

6. Alternatywna składnia *ngFor (starsze projekty)

W starszych projektach może spotkasz tradycyjną składnię *ngFor. Warto ją znać, ale zalecamy używanie @for w nowych projektach.

Porównanie składni

// Komponent wspólny dla obu wersji
export class ComparisonComponent {
  filmy = [
    { id: 1, title: 'Avengers', year: 2019 },
    { id: 2, title: 'Spider-Man', year: 2021 }
  ];
}

Wersja z *ngFor (STARSZA)

<!-- Wymaga import { CommonModule } w komponencie! -->
<div *ngFor="let film of filmy; let i = index;" class="card mb-2">
  <div class="card-body">
    <h5>{{ film.title }}</h5>
    <p>Rok: {{ film.year }}</p>
    <small>Pozycja: {{ i + 1 }}</small>
  </div>
</div>

<!-- Dla pustej listy trzeba osobny *ngIf -->
<div *ngIf="filmy.length === 0">
  <p>Brak filmów do wyświetlenia</p>
</div>

Kluczowe różnice

Cecha@for (NOWA)*ngFor (STARA)
ImportNie wymaga importówWymaga CommonModule
Składnia@for (item of items; track item.id)*ngFor="let item of items; trackBy: fn"
TrackWbudowane w składnięWymaga osobnej funkcji
EmptyWbudowane @emptyWymaga osobnego *ngIf
Zmienne$index, $count itp.let i = index itp.
CzytelnośćBardzo czytelneMniej czytelne
WydajnośćLepszaDobra

7. Jak Angular obsługuje pętle

Gdy używasz @for, Angular:

  1. Iteruje przez tablicę i tworzy element DOM dla każdego elementu
  2. Używa track do identyfikacji, które elementy się zmieniły
  3. Aktualizuje tylko zmienione elementy zamiast przebudowywać całą listę
  4. Pamięta pozycje i nie przesuwa elementów niepotrzebnie

Dlaczego track jest ważne:

  • Bez track Angular musi odbudować całą listę przy każdej zmianie
  • Z track Angular wie, który element się zmienił i aktualizuje tylko jego
  • To znacznie przyspiesza działanie aplikacji przy dużych listach

8. Najlepsze praktyki

✅ Co robić:

  1. Używaj @for w nowych projektach – jest prostszy i lepszy
  2. Zawsze używaj track – poprawia wydajność Angulara
  3. Używaj unikalnych ID dla tracktrack item.id zamiast track $index
  4. Używaj @empty – dla lepszego UX gdy lista jest pusta
  5. Dawaj sensowne nazwy zmiennymfilm zamiast item

❌ Czego unikać:

  1. Zapominania o track – Angular będzie pokazywał ostrzeżenia
  2. Używania track $index – gdy masz dostęp do unikalnego ID
  3. Zbyt zagnieżdżonych pętli – lepiej podziel na komponenty
  4. Modyfikowania tablicy podczas iteracji – może powodować błędy

Przykład dobrej praktyki:

// ✅ Dobra praktyka
get filtrowane(): Film[] {
  return this.filmy.filter(film => film.rating >= this.minRating);
}
<!-- ✅ Używaj metod/getterów dla złożonej logiki -->
@for (film of filtrowane; track film.id) {
  <div>{{ film.title }}</div>
}

9. Ćwiczenia do samodzielnego wykonania

  • Lista użytkowników: Wyświetl użytkowników z różnymi rolami i statusami
  • Lista zadań: Stwórz listę zadań z możliwością dodawania i usuwania
  • Tabela wyników: Wyświetl tabelę z wynikami gier/meczów z sortowaniem
  • Galeria zdjęć: Lista zdjęć z opisami (użyj URLs do obrazków)
  • Menu restauracji: Kategorie dań z zagnieżdżonymi listami pozycji