Lange bevor Angular 17 den Built-in Control Flow (@if, @for, @switch) brachte, waren *ngIf, *ngFor und *ngSwitch der Standard für bedingtes Rendern und Listen. In riesigen Bestands-Codebasen sind sie es bis heute. Das Angular-Team hat sie seit v20 zwar als deprecated markiert – aber „deprecated” heißt hier ausdrücklich nicht „bald entfernt”: Sie funktionieren weiterhin vollständig, werden weiter unterstützt und bleiben in jedem Tutorial und jeder Library-Doku der Welt präsent. Wer Angular ernsthaft schreibt oder liest, kommt an ihnen nicht vorbei. Dieser Artikel zeigt jede Direktive im Detail, das Desugaring der Microsyntax, eigene Strukturdirektiven und den Migrationspfad zum neuen Control Flow.
Strukturdirektiven verstehen
Eine Strukturdirektive manipuliert die DOM-Struktur ihres Host-Elements – sie fügt Elemente ein, entfernt sie oder vervielfältigt sie. Anders als Attribute-Direktiven (die Eigenschaften eines bestehenden Elements verändern) entscheidet eine Strukturdirektive, ob und wie oft das Element überhaupt gerendert wird.
Erkennbar am Asterisk-Präfix: *ngIf, *ngFor, *ngSwitchCase. Der Stern ist Syntactic Sugar des Angular-Compilers für ein wrapping <ng-template>-Element. Die Direktive selbst arbeitet auf diesem Template, instantiiert es bei Bedarf in einen ViewContainerRef und entfernt die Instanz wieder, wenn die Bedingung wegfällt.
*ngIf – Grundlagen und Varianten
*ngIf rendert sein Host-Template, wenn der Ausdruck truthy ist. Drei Eingaben sind möglich: ngIf (der Ausdruck), ngIfThen und ngIfElse (jeweils ein TemplateRef). In Standalone-Komponenten musst du NgIf oder CommonModule explizit importieren.
import { Component, signal } from '@angular/core';
import { NgIf, AsyncPipe } from '@angular/common';
import { Observable, of } from 'rxjs';
interface User { name: string; role: string; }
@Component({
selector: 'app-ngif-demo',
standalone: true,
imports: [NgIf, AsyncPipe],
template: `
<!-- Variante 1: Einfaches Rendern -->
<p *ngIf="loggedIn()">Willkommen zurück.</p>
<!-- Variante 2: if/else mit ng-template -->
<div *ngIf="loggedIn(); else guestBlock">
<button (click)="loggedIn.set(false)">Abmelden</button>
</div>
<ng-template #guestBlock>
<button (click)="loggedIn.set(true)">Anmelden</button>
</ng-template>
<!-- Variante 3: then/else explizit -->
<div *ngIf="loggedIn(); then loggedTpl else guestTpl"></div>
<ng-template #loggedTpl><p>Eingeloggt</p></ng-template>
<ng-template #guestTpl><p>Gast</p></ng-template>
<!-- Variante 4: as-Variable für Async-Pipe -->
<section *ngIf="user$ | async as user">
<h2>{{ user.name }}</h2>
<p>Rolle: {{ user.role }}</p>
</section>
`
})
export class NgIfDemoComponent {
loggedIn = signal(true);
user$: Observable<User> = of({ name: 'Mia', role: 'Admin' });
}Variante 4 ist der wichtigste Einsatzfall: Mit as user wird der Async-Pipe-Wert in einer Template-Variable gespeichert und ist im ganzen Block wiederverwendbar. Ohne as müsstest du user$ | async mehrfach schreiben – jedes Mal wäre eine neue Subscription die Folge.
*ngFor mit allen Helfern und trackBy
*ngFor iteriert über jedes Iterable<T> (Array, Map, Set). Neben dem Element selbst stellt die Direktive sieben Kontextvariablen bereit, die du per as-Pattern in lokale Template-Variablen exportierst.
| Variable | Typ | Bedeutung |
|---|---|---|
index | number | 0-basierter Index |
count | number | Länge der Iterable |
first | boolean | Erstes Item? |
last | boolean | Letztes Item? |
even | boolean | Gerader Index? |
odd | boolean | Ungerader Index? |
$implicit | T | Das Item selbst (let item) |
import { Component, signal } from '@angular/core';
import { NgFor, NgClass } from '@angular/common';
interface User { id: string; name: string; }
@Component({
selector: 'app-user-list',
standalone: true,
imports: [NgFor, NgClass],
template: `
<ul>
<li
*ngFor="let user of users();
let i = index;
let first = first;
let last = last;
let odd = odd;
trackBy: trackById"
[ngClass]="{ first, last, odd }"
>
{{ i + 1 }}. {{ user.name }}
<button (click)="remove(user.id)">Entfernen</button>
</li>
</ul>
<button (click)="add()">User hinzufügen</button>
`
})
export class UserListComponent {
users = signal<User[]>([
{ id: 'u1', name: 'Alice' },
{ id: 'u2', name: 'Bob' },
{ id: 'u3', name: 'Charlie' }
]);
// Identitäts-Tracking via stabile ID
trackById = (_: number, u: User) => u.id;
add() {
this.users.update(list => [...list, {
id: crypto.randomUUID(),
name: 'Neu ' + (list.length + 1)
}]);
}
remove(id: string) {
this.users.update(list => list.filter(u => u.id !== id));
}
}trackBy ist bei kleinen Listen optional, bei großen oder häufig wechselnden Listen aber Pflicht: Ohne trackBy vergleicht Angular per Objekt-Identität. Wird das Array komplett neu zugewiesen (z.B. nach einem HTTP-Reload), hält Angular jedes Item für „neu” und rendert das gesamte DOM neu – inklusive aller Child-Komponenten und Animationen. Mit trackBy auf eine stabile ID erkennt Angular „gleiches Objekt, anderer Snapshot” und erhält die DOM-Knoten.
*ngSwitch / *ngSwitchCase / *ngSwitchDefault
ngSwitch arbeitet als Triple: Ein Container-Element trägt [ngSwitch]="expr" (kein Asterisk – das ist eine reguläre Property-Bindung), darunter stehen Kindelemente mit *ngSwitchCase="value" und optional eines mit *ngSwitchDefault.
import { Component, input } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled';
@Component({
selector: 'app-status-badge',
standalone: true,
imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
template: `
<div [ngSwitch]="status()" class="badge">
<span *ngSwitchCase="'pending'" class="badge-warn">
Warten auf Zahlung
</span>
<span *ngSwitchCase="'paid'" class="badge-info">
Bezahlt
</span>
<span *ngSwitchCase="'shipped'" class="badge-success">
Versendet
</span>
<span *ngSwitchCase="'cancelled'" class="badge-error">
Storniert
</span>
<span *ngSwitchDefault class="badge-neutral">
Unbekannt
</span>
</div>
`
})
export class StatusBadgeComponent {
status = input.required<OrderStatus>();
}Im Vergleich zu @switch ist die ngSwitch-Variante deutlich verbose – drei Direktiven statt einer Block-Syntax, und der Compiler kann nicht type-narrowen. Genau das hat das Team bei @switch deutlich verbessert.
*ngTemplateOutlet und <ng-container>
Zwei Helfer, die häufig zusammen mit den drei Hauptdirektiven auftauchen:
<ng-container>ist ein logischer Wrapper, der kein DOM-Element erzeugt. Ideal, um Strukturdirektiven anzuwenden, ohne ein zusätzliches<div>ins DOM zu pumpen.*ngTemplateOutletrendert einTemplateRef– optional mit einem Context-Object, dessen Properties als Template-Variablen verfügbar werden.
import { Component, signal } from '@angular/core';
import { NgFor, NgIf, NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-template-outlet-demo',
standalone: true,
imports: [NgFor, NgIf, NgTemplateOutlet],
template: `
<!-- ng-container: kein zusätzliches DOM-Element -->
<ng-container *ngIf="users().length > 0; else emptyTpl">
<h2>Nutzer</h2>
<ul>
<li *ngFor="let u of users()">{{ u }}</li>
</ul>
</ng-container>
<!-- TemplateOutlet mit Context -->
<ng-template #greetingTpl let-name="name" let-role="role">
<p>Hallo {{ name }} ({{ role }})</p>
</ng-template>
<ng-container
*ngTemplateOutlet="greetingTpl;
context: { name: 'Mia', role: 'Admin' }">
</ng-container>
<ng-template #emptyTpl>
<p>Keine Nutzer vorhanden.</p>
</ng-template>
`
})
export class TemplateOutletDemoComponent {
users = signal(['Alice', 'Bob']);
}*ngTemplateOutlet ist außerdem das Mittel der Wahl, um polymorphe Listen zu rendern – also pro Item-Typ ein anderes Template zu wählen.
Microsyntax-Desugaring
Der Asterisk ist reine Compiler-Magie. So expandiert Angular *ngIf:
<!-- Du schreibst: -->
<div *ngIf="user; else guestBlock">
{{ user.name }}
</div>
<ng-template #guestBlock>Gast</ng-template>
<!-- Der Compiler macht daraus: -->
<ng-template [ngIf]="user" [ngIfElse]="guestBlock">
<div>{{ user.name }}</div>
</ng-template>
<ng-template #guestBlock>Gast</ng-template>Analog für *ngFor:
<!-- Microsyntax -->
<li *ngFor="let item of items; let i = index; trackBy: trackById">
{{ i }}: {{ item.name }}
</li>
<!-- Expandiert zu -->
<ng-template ngFor
let-item
[ngForOf]="items"
let-i="index"
[ngForTrackBy]="trackById">
<li>{{ i }}: {{ item.name }}</li>
</ng-template>Genau aus diesem Grund ist nur eine Strukturdirektive pro Element erlaubt: Der Stern verlangt einen einzelnen impliziten <ng-template>-Wrapper. Zwei Sterne würden zwei Wrapper auf einer Position fordern – nicht repräsentierbar.
Eigene Strukturdirektiven schreiben
Mit TemplateRef und ViewContainerRef baust du in zehn Zeilen eigene Strukturdirektiven. Der Klassiker: ein invertiertes *ngIf namens *appUnless.
import {
Directive,
Input,
TemplateRef,
ViewContainerRef
} from '@angular/core';
@Directive({
selector: '[appUnless]',
standalone: true
})
export class UnlessDirective {
private hasView = false;
constructor(
private tpl: TemplateRef<unknown>,
private vcr: ViewContainerRef
) {}
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.vcr.createEmbeddedView(this.tpl);
this.hasView = true;
} else if (condition && this.hasView) {
this.vcr.clear();
this.hasView = false;
}
}
}Verwendung im Template:
<p *appUnless="isLoggedIn()">
Bitte melde dich an, um Inhalte zu sehen.
</p>Wichtig: Der Selector muss in eckigen Klammern stehen und die @Input-Property muss exakt den gleichen Namen wie der Selector tragen. Der Compiler verbindet beides automatisch via Microsyntax.
Migration zu @if / @for / @switch
Angular liefert ein automatisches Migrationsschematic mit. Es konvertiert in den meisten Fällen 1:1:
ng generate @angular/core:control-flowVorher/Nachher im direkten Vergleich:
Klassisch (*ngIf/*ngFor/*ngSwitch) | Built-in Control Flow |
|---|---|
<div *ngIf="x">...</div> | @if (x) { <div>...</div> } |
<div *ngIf="x; else other">A</div><ng-template #other>B</ng-template> | @if (x) { A } @else { B } |
<li *ngFor="let i of items; trackBy: byId"> | @for (i of items; track i.id) { <li>... } |
[ngSwitch] mit *ngSwitchCase/*ngSwitchDefault | @switch (x) { @case (...) {} @default {} } |
CommonModule muss importiert sein | Kein Import nötig |
Was das Schematic nicht zuverlässig erledigt:
- Komplexe
then/else-Patterns mit drei oder mehr Templates – die musst du manuell zusammenfassen. trackBy-Funktionen werden 1:1 übernommen, aber bei@foristtrackPflicht und nutzt einen direkt eingebetteten Ausdruck (track item.id) statt einer separaten Methode.- Kombinationen aus
*ngIfund*ngForüber<ng-container>werden korrekt entwirrt, aber das Ergebnis wirkt manchmal unidiomatisch – ein zweiter Blick lohnt.
Häufige Stolperfallen
Zwei Strukturdirektiven auf einem Element
<div *ngIf=“x” *ngFor=“let i of items”> bricht den Build mit einer kryptischen „Can’t have multiple template bindings”-Meldung. Lösung: Verschachtelung über <ng-container *ngIf=“x”><div *ngFor=“let i of items”>…</div></ng-container> oder direkt der neue Built-in Control Flow.
*ngFor ohne trackBy bei großen Listen
Wechselt die Array-Reference (z. B. nach einem Reload), behandelt Angular ohne trackBy jedes Item als neu – das gesamte DOM wird neu aufgebaut, alle Child-Komponenten remountet. Bei Listen ab ~50 Items spürbar lahm. Eine simple trackBy: byId-Funktion löst das vollständig.
CommonModule fehlt in Standalone-Component
Fehlermeldung „Can’t bind to ngForOf since it isn’t a known property of li” oder „NG8002” deutet fast immer auf einen fehlenden Import hin. In Standalone-Components musst du NgIf, NgFor, NgSwitch einzeln importieren oder pauschal CommonModule. Beim Built-in Control Flow entfällt das.
Verschachtelte ng-template-Else-Blöcke werden unleserlich
Drei oder mehr verschachtelte *ngIf=”…; else …” mit eigenen <ng-template>-Definitionen weiter unten im Template kippen die Lesbarkeit. Spätestens hier lohnt sich der Wechsel zu @switch, eine eigene Komponente oder eine Computed-Signal-getriebene String-Auswahl.
*ngFor-Variablen sind nur im Template verfügbar
let i = index oder let last existieren ausschließlich im Template-Scope. Aus dem Component-Code (oder per @ViewChild) kommst du nicht an sie heran. Wer den Index außerhalb braucht, muss ihn beim Eventhandling explizit mitschicken: (click)=“select(item, i)”.
*ngTemplateOutlet wird unterschätzt
Mit Context-Object und einer Map von Templates pro Item-Typ rendert *ngTemplateOutlet heterogene Listen (Headlines, Cards, Ads, …) im selben *ngFor – ohne hässliche *ngIf-Kaskaden. Lese-Tipp: ngTemplateOutletContext akzeptiert exakt dasselbe Format wie die Variablen-Bindings im <ng-template>.
Migration ist konservativ – nicht erschöpfend
Das ng generate @angular/core:control-flow-Schematic deckt 80–90 % der Fälle. Bei vielen verschachtelten *ngIf+*ngFor+else-Templates bleibt manuelle Nacharbeit. Plane bei großen Codebasen einen dedizierten Refactor-Sprint statt einer Drive-by-Migration.
Weiterführende Ressourcen
Externe Quellen
- NgIf API – Angular.dev
- NgFor API – Angular.dev
- Control Flow Migration – Angular.dev
- Structural Directives Guide – Angular.dev