Die Blöcke @if, @for und @switch sind die native Kontrollsyntax von Templates (ab v17, stabil ab v18). Sie ersetzen die strukturellen Direktiven *ngIf, *ngFor und ngSwitch: Sie müssen nicht importiert werden, sind besser typisiert, schneller und leichter lesbar.
@if: Bedingungen, else if, Alias
@if (user(); as u) {
<p>Hallo, {{ u.name }}!</p>
} @else if (loading()) {
<app-spinner />
} @else {
<a routerLink="/login">Anmelden</a>
}
<!-- `as` erzeugt einen typisierten (nicht-null) Alias für den aktuellen Block -->
@if (order()?.items?.length; as count) {
<span>{{ count }} Artikel</span>
}@for: Listen mit obligatorischem track
track ist Pflicht. Es sagt Angular, wie es ein Element zwischen zwei Renderings wiedererkennt, damit das DOM wiederverwendet statt neu erzeugt wird. Der Block @empty erscheint, wenn die Liste leer ist.
<ul>
@for (task of tasks(); track task.id; let i = $index, last = $last) {
<li [class.last]="last">{{ i + 1 }}. {{ task.title }}</li>
} @empty {
<li>Du hast keine Aufgaben.</li>
}
</ul>
<!-- In @for verfügbare Variablen:
$index $count $first $last $even $odd -->- 1/3 · *ngIf auf @if migrieren$first
- 2/3 · track zu jedem @for hinzufügen
- 3/3 · CommonModule entfernen$last
Wie du den track-Ausdruck wählst
// ✓ Stabile, eindeutige id — in 95 % der Fälle die richtige Wahl
@for (user of users(); track user.id) { ... }
// ✓ Eindeutige primitive Werte (Strings, Zahlen)
@for (tag of tags(); track tag) { ... }
// $index — nur für statische Listen, die nie umsortiert / gefiltert werden
@for (step of wizardSteps; track $index) { ... }
// ✗ Das ganze Objekt (Identität), wenn die Liste unveränderlich neu erzeugt wird oder vom Server kommt:
// jedes erneute Laden = neue Objekte = das GESAMTE DOM wird zerstört und neu erzeugt
@for (user of users(); track user) { ... }1. Schreib eine Notiz neben Ana. 2. Klick auf „Ersten ans Ende verschieben“. Wo ist die Notiz gelandet?
Einen ganzen Katalog mit Suche, Sortierung und Seiten, nach id getrackt, findest du unter Listen. Derselbe $index-Bug ist einer der vier in Deine erste App.
@switch, mehrere Cases und Exhaustive Checkingv21
@switch (user().role) {
@case ('admin') { <app-admin-dashboard /> }
@case ('editor') { <app-editor-dashboard /> }
@default { <app-viewer-dashboard /> }
}Ab v21.1 kannst du mehrere aufeinanderfolgende @case-Blöcke für denselben Inhalt zusammenfassen, und ab v21.2 macht @default never; einen vergessenen Case zu einem Compile-Fehler.
<!-- v21.1: mehrere aufeinanderfolgende @case-Blöcke für denselben Inhalt -->
<!-- v21.2: @default never; → COMPILE-Fehler, wenn du einen Case vergisst -->
@let current = status();
@switch (current) {
@case ('draft')
@case ('pending') { <span>Ausstehend</span> }
@case ('paid') { <span>Bezahlt</span> }
@case ('shipped')
@case ('delivered') { <span>Versendet</span> }
@case ('cancelled') { <span>Storniert</span> }
@default never;
}Alte Syntax und Migration
<!-- ✗ Alte Syntax (funktioniert noch, gilt stilistisch aber als veraltet) -->
<div *ngIf="user$ | async as user; else loading">{{ user.name }}</div>
<ng-template #loading><app-spinner /></ng-template>
<li *ngFor="let t of tasks; trackBy: trackById; let i = index">{{ t.title }}</li>
<div [ngSwitch]="role">
<app-admin *ngSwitchCase="'admin'" />
<app-viewer *ngSwitchDefault />
</div>Führe ng generate @angular/core:control-flow aus — die Migration läuft automatisch und ist sicher. Danach kannst du CommonModule / NgIf / NgFor meist aus den imports entfernen.
Häufige Stolperfallen
track $indexbei dynamischen Listen: Der DOM-Zustand (Inputs, Fokus, Animationen) bleibt an der Position hängen, nicht am Element.track itembei unveränderlichen Daten: Jedes Update erzeugt neue Objekte → das gesamte DOM wird neu erzeugt.- Teure Aufrufe in Bedingungen:
@if (computeSomething())läuft bei jeder Change Detection. Verlagere die Logik in eincomputed(). @switch (status())+@default never: Der Typ wird bei einem Signal-Aufruf nicht eingeengt; nutze@let s = status();und@switch (s).- Sonderzeichen: In einem Template müssen ein literales
@und{ }als Entities geschrieben werden (@,{).
Spickzettel
@if (cond) { } @else if (other) { } @else { }
@if (expr; as alias) { }
@for (item of items; track item.id; let i = $index, f = $first) { } @empty { }
// $index $count $first $last $even $odd
@switch (value) {
@case (a) @case (b) { } // v21.1: mehrere Cases
@default { } // oder: @default never; (v21.2, exhaustiv)
}
// Migration: ng generate @angular/core:control-flow