ng-template, ng-container i ng-content w Angularze

1. Czym są elementy szablonowe?

W Angularze istnieją specjalne elementy, które pomagają w zarządzaniu strukturą szablonów HTML. Nie są to zwykłe elementy DOM – to narzędzia Angulara do kontrolowania tego, jak i gdzie treść jest wyświetlana.

Trzy główne elementy szablonowe:

  • <ng-template> – szablon, który nie jest wyświetlany domyślnie
  • <ng-container> – grupuje elementy bez dodawania dodatkowego elementu DOM
  • <ng-content> – pozwala na wstawianie treści do komponentu z zewnątrz

Dlaczego są potrzebne?

  • Lepsze zarządzanie strukturą HTML
  • Unikanie niepotrzebnych elementów DOM
  • Tworzenie elastycznych komponentów
  • Warunkowe wyświetlanie treści

2. ng-template – szablony do wielokrotnego użytku

<ng-template> to element, który definiuje szablon HTML, ale nie wyświetla go automatycznie. To jak „przepis” na treść, którą możemy użyć w różnych miejscach.

Podstawowe użycie ng-template

// film-template.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-film-template',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './film-template.component.html',
  styleUrl: './film-template.component.css'
})
export class FilmTemplateComponent {
  showFilm = false;
  film = {
    title: 'Avengers: Endgame',
    year: 2019,
    rating: 8.4
  };
  
  toggleFilm() {
    this.showFilm = !this.showFilm;
  }
}
<!-- film-template.component.html -->
<div class="container">
  <h2>Przykład ng-template</h2>
  
  <button (click)="toggleFilm()" class="btn btn-primary">
    @if (showFilm) {
      Ukryj film
    } @else {
      Pokaż film
    }
  </button>
  
  <!-- Używanie ng-template z @if (nowoczesny sposób) -->
  @if (showFilm) {
    <ng-container [ngTemplateOutlet]="filmTemplate"></ng-container>
  } @else {
    <ng-container [ngTemplateOutlet]="noFilmTemplate"></ng-container>
  }
  
  <!-- Definicja szablonów -->
  <ng-template #filmTemplate>
    <div class="alert alert-success">
      <h4>{{ film.title }}</h4>
      <p>Rok: {{ film.year }}</p>
      <p>Ocena: {{ film.rating }}/10</p>
    </div>
  </ng-template>
  
  <ng-template #noFilmTemplate>
    <div class="alert alert-info">
      <p>Kliknij przycisk, aby zobaczyć film!</p>
    </div>
  </ng-template>
</div>

Przykład z listą filmów i szablonami

// film-list-templates.component.ts
export class FilmListTemplatesComponent {
  filmy = [
    { id: 1, title: 'Avengers', rating: 8.4, type: 'akcja' },
    { id: 2, title: 'Titanic', rating: 7.8, type: 'romans' },
    { id: 3, title: 'Joker', rating: 8.5, type: 'dramat' },
    { id: 4, title: 'Scary Movie', rating: 6.2, type: 'komedia' }
  ];
  
  selectedTemplate = 'card';
  
  changeTemplate(templateName: string) {
    this.selectedTemplate = templateName;
  }
}
<!-- film-list-templates.component.html -->
<div class="container">
  <h2>Lista filmów z różnymi szablonami</h2>
  
  <!-- Przełączanie szablonów -->
  <div class="mb-3">
    <button (click)="changeTemplate('card')" 
            [class.active]="selectedTemplate === 'card'"
            class="btn btn-outline-primary me-2">
      Widok karty
    </button>
    <button (click)="changeTemplate('list')" 
            [class.active]="selectedTemplate === 'list'"
            class="btn btn-outline-primary me-2">
      Widok listy
    </button>
    <button (click)="changeTemplate('table')" 
            [class.active]="selectedTemplate === 'table'"
            class="btn btn-outline-primary">
      Widok tabeli
    </button>
  </div>
  
  <!-- Wyświetlanie filmów z wybranym szablonem -->
  <div class="films-container">
    @for (film of filmy; track film.id) {
      @if (selectedTemplate === 'card') {
        <ng-container [ngTemplateOutlet]="cardTemplate" 
                      [ngTemplateOutletContext]="{film: film}">
        </ng-container>
      } @else if (selectedTemplate === 'list') {
        <ng-container [ngTemplateOutlet]="listTemplate" 
                      [ngTemplateOutletContext]="{film: film}">
        </ng-container>
      } @else if (selectedTemplate === 'table') {
        <ng-container [ngTemplateOutlet]="tableRowTemplate" 
                      [ngTemplateOutletContext]="{film: film}">
        </ng-container>
      }
    }
  </div>
  
  <!-- Szablony -->
  <ng-template #cardTemplate let-film="film">
    <div class="card mb-3" style="width: 18rem;">
      <div class="card-body">
        <h5 class="card-title">{{ film.title }}</h5>
        <p class="card-text">
          <span class="badge bg-secondary">{{ film.type }}</span>
        </p>
        <p class="card-text">
          <strong>Ocena:</strong> {{ film.rating }}/10
        </p>
      </div>
    </div>
  </ng-template>
  
  <ng-template #listTemplate let-film="film">
    <div class="list-group-item">
      <div class="d-flex w-100 justify-content-between">
        <h6 class="mb-1">{{ film.title }}</h6>
        <small>{{ film.rating }}/10</small>
      </div>
      <small class="text-muted">{{ film.type }}</small>
    </div>
  </ng-template>
  
  <ng-template #tableRowTemplate let-film="film">
    <tr>
      <td>{{ film.title }}</td>
      <td>{{ film.type }}</td>
      <td>{{ film.rating }}/10</td>
    </tr>
  </ng-template>
</div>

3. ng-container – grupowanie bez dodatkowego DOM

<ng-container> to element, który pozwala grupować inne elementy bez dodawania dodatkowego elementu DOM. Jest niewidoczny w końcowym HTML.

Problem: niepotrzebne elementy DOM

<!-- PROBLEM: dodatkowy div tylko dla @if -->
<div *ngIf="showContent">
  <h3>Tytuł</h3>
  <p>Treść</p>
</div>

Rozwiązanie: ng-container

<!-- ROZWIĄZANIE: brak dodatkowego elementu DOM -->
<ng-container *ngIf="showContent">
  <h3>Tytuł</h3>
  <p>Treść</p>
</ng-container>

Praktyczne przykłady ng-container

// film-container.component.ts
export class FilmContainerComponent {
  films = [
    { id: 1, title: 'Avengers', category: 'akcja', rating: 8.4, isNew: true },
    { id: 2, title: 'Titanic', category: 'romans', rating: 7.8, isNew: false },
    { id: 3, title: 'Joker', category: 'dramat', rating: 8.5, isNew: true }
  ];
  
  showCategories = true;
  selectedCategory = '';
  
  toggleCategories() {
    this.showCategories = !this.showCategories;
  }
  
  setCategory(category: string) {
    this.selectedCategory = category;
  }
}
<!-- film-container.component.html -->
<div class="container">
  <h2>Przykłady ng-container</h2>
  
  <!-- Przykład 1: Grupowanie elementów bez dodatkowego DOM -->
  <div class="mb-4">
    <h3>Nagłówek sekcji</h3>
    
    @if (showCategories) {
      <ng-container>
        <p>Wybierz kategorię filmu:</p>
        <button (click)="setCategory('akcja')" class="btn btn-sm btn-outline-danger me-2">
          Akcja
        </button>
        <button (click)="setCategory('romans')" class="btn btn-sm btn-outline-pink me-2">
          Romans
        </button>
        <button (click)="setCategory('dramat')" class="btn btn-sm btn-outline-secondary">
          Dramat
        </button>
      </ng-container>
    }
    
    <button (click)="toggleCategories()" class="btn btn-primary mt-2">
      {{ showCategories ? 'Ukryj' : 'Pokaż' }} kategorie
    </button>
  </div>
  
  <!-- Przykład 2: ng-container z @for -->
  <div class="mb-4">
    <h3>Lista filmów</h3>
    
    @for (film of films; track film.id) {
      <ng-container>
        <!-- Sprawdzamy czy film pasuje do wybranej kategorii -->
        @if (!selectedCategory || film.category === selectedCategory) {
          <div class="card mb-2">
            <div class="card-body">
              <h5 class="card-title">
                {{ film.title }}
                @if (film.isNew) {
                  <span class="badge bg-success ms-2">NOWOŚĆ!</span>
                }
              </h5>
              <p class="card-text">
                <span class="badge bg-info">{{ film.category }}</span>
                <span class="ms-2">Ocena: {{ film.rating }}/10</span>
              </p>
            </div>
          </div>
        }
      </ng-container>
    }
  </div>
  
  <!-- Przykład 3: Złożone warunki -->
  <div class="mb-4">
    <h3>Filmy z wysoką oceną</h3>
    
    @for (film of films; track film.id) {
      @if (film.rating >= 8.0) {
        <ng-container>
          <div class="alert alert-warning">
            <strong>{{ film.title }}</strong> - Wysoko oceniony film!
            @if (film.isNew) {
              <br><small>Dodatkowo: to nowość w naszej kolekcji!</small>
            }
          </div>
        </ng-container>
      }
    }
  </div>
</div>

4. ng-content – projekcja treści (content projection)

<ng-content> pozwala na wstawianie treści do komponentu z zewnątrz. To znaczy, że komponent może mieć „dziury”, które wypełniamy treścią podczas używania komponentu.

Podstawowy przykład ng-content

// film-card.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-film-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[slot=header]"></ng-content>
      </div>
      <div class="card-body">
        <ng-content></ng-content>
      </div>
      <div class="card-footer">
        <ng-content select="[slot=footer]"></ng-content>
      </div>
    </div>
  `,
  styles: [`
    .card {
      border: 1px solid #ddd;
      border-radius: 8px;
      margin: 16px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .card-header {
      background: #f8f9fa;
      padding: 16px;
      border-bottom: 1px solid #ddd;
    }
    .card-body {
      padding: 16px;
    }
    .card-footer {
      background: #f8f9fa;
      padding: 12px 16px;
      border-top: 1px solid #ddd;
    }
  `]
})
export class FilmCardComponent { }

Używanie komponentu z ng-content

// parent.component.ts
import { Component } from '@angular/core';
import { FilmCardComponent } from './film-card/film-card.component';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [FilmCardComponent],
  templateUrl: './parent.component.html'
})
export class ParentComponent {
  films = [
    { id: 1, title: 'Avengers: Endgame', year: 2019, rating: 8.4 },
    { id: 2, title: 'The Godfather', year: 1972, rating: 9.2 }
  ];
}
<!-- parent.component.html -->
<div class="container">
  <h2>Przykłady ng-content</h2>
  
  <!-- Użycie 1: Podstawowa karta filmu -->
  <app-film-card>
    <h3 slot="header">🎬 Avengers: Endgame</h3>
    
    <p>Finalna walka z Thanosem. Bohaterowie próbują odwrócić skutki Blip.</p>
    <p><strong>Rok:</strong> 2019</p>
    <p><strong>Ocena:</strong> 8.4/10</p>
    
    <button slot="footer" class="btn btn-primary">Zobacz szczegóły</button>
  </app-film-card>
  
  <!-- Użycie 2: Inna zawartość -->
  <app-film-card>
    <h3 slot="header">🏆 The Godfather</h3>
    
    <div>
      <p>Klasyka kina gangsterskiego. Historia rodziny Corleone.</p>
      <div class="badge bg-success">Klasyka</div>
      <div class="badge bg-warning ms-2">Dramat</div>
    </div>
    
    <div slot="footer">
      <button class="btn btn-success me-2">⭐ Dodaj do ulubionych</button>
      <button class="btn btn-outline-secondary">Więcej info</button>
    </div>
  </app-film-card>
  
  <!-- Użycie 3: Minimalna zawartość -->
  <app-film-card>
    <h4 slot="header">Film bez szczegółów</h4>
    <p>Tylko podstawowe informacje.</p>
  </app-film-card>
</div>

5. Porównanie i kiedy używać

ng-template

Kiedy używać:

  • Chcesz zdefiniować szablon do wielokrotnego użytku
  • Potrzebujesz warunkowego wyświetlania z różnymi szablonami
  • Tworzysz dynamiczne komponenty

Przykład zastosowania:

  • Różne widoki dla różnych stanów (loading, error, success)
  • Szablony dla różnych typów użytkowników
  • Części formularza pokazywane warunkowo
ng-container

Kiedy używać:

  • Potrzebujesz grupować elementy bez dodawania dodatkowego DOM
  • Używasz dyrektyw strukturalnych (@if, @for) ale nie chcesz owijać w dodatkowy element
  • Chcesz zastosować wiele dyrektyw na tej samej grupie elementów

Przykład zastosowania:

  • Warunkowe wyświetlanie grupy elementów
  • Optymalizacja DOM (mniej zbędnych elementów)
  • Złożone warunki logiczne w szablonach
ng-content

Kiedy używać:

  • Tworzysz komponenty wielokrotnego użytku
  • Chcesz, żeby komponent mógł przyjmować różną treść
  • Budujesz komponenty UI (karty, modale, panele)

Przykład zastosowania:

  • Komponenty layout (header, sidebar, footer)
  • Komponenty UI (przyciski, karty, modale)
  • Komponenty owijające (kontenery, panele)

6. Najlepsze praktyki

✅ Co robić:
  1. Używaj ng-container zamiast dodatkowych div-ów dla grup elementów
  2. Nazywaj ng-template opisowo#loadingTemplate nie #temp1
  3. Używaj slotów w ng-content dla lepszej kontroli (select="[slot=header]")
  4. Twórz komponenty z ng-content dla lepszej reużywalności
  5. Dokumentuj sloty w komponentach dla innych deweloperów
❌ Czego unikać:
  1. Nadużywania ng-template dla prostych przypadków
  2. Tworzenia zbyt zagnieżdżonych struktur szablonowych
  3. Zapominania o dostępności w komponentach z ng-content
  4. Używania ng-container gdy potrzebujesz rzeczywisty element DOM

7. Ćwiczenia do samodzielnego wykonania

  1. Komponent karty produktu – z ng-content dla obrazka, tytułu i opisów
  2. System powiadomień – z ng-template dla różnych typów alertów
  3. Komponent tabs – z ng-content dla zawartości każdej zakładki
  4. Layout strony – z ng-content dla header, main, sidebar, footer
  5. Komponent formularza – z ng-template dla różnych typów pól