Die Template-Ausdruckssprache hat sich in den letzten Versionen deutlich an TypeScript angenähert. Neben den lokalen Variablen @let hast du jetzt Template Literals, Regex, Spread, Arrow Functions, instanceof und sogar Kommentare innerhalb von Tags. Das Ziel: weniger „Glue“-Code in der Klasse für einfache Dinge. Die Grundlagen darunter — {{ }}, [ ], ( ) und [( )] — stehen unter Data Binding.
| Feature | Version | Beispiel |
|---|---|---|
@let | v18.1 (stabil ab v19) | @let total = price() * qty(); |
| Template Literals | v20 | {{ `Hi ${name()}` }} |
**, in, void, typeof | v19–v20 | 'x' in obj |
| Reguläre Ausdrücke | /^\d+$/.test(v) | |
| Spread (Array, Objekt, Argumente) | [...a, ...b] | |
| Arrow Functions | items.filter(i => i.ok) | |
instanceof | err instanceof HttpErrorResponse | |
| Kommentare innerhalb von Tags | <div // note | |
?. liefert undefined | user?.name |
@let: lokale Variablen in Templates
@let deklariert eine schreibgeschützte Variable, die im aktuellen Block und in dessen Kind-Blöcken sichtbar ist. Sie wird bei jedem Rendern automatisch neu berechnet. Perfekt, um die Wiederholung eines langen Ausdrucks oder einer mehrfach verwendeten async-Pipe zu vermeiden.
<!-- @let deklariert eine lokale, schreibgeschützte Variable, sichtbar im aktuellen Block und in dessen Kindern -->
@let user = user$ | async;
@let fullName = user?.firstName + ' ' + user?.lastName;
@let isAdmin = user?.roles?.includes('admin') ?? false;
<h1>{{ fullName }}</h1>
@if (isAdmin) { <app-admin-tools /> }
<!-- ideal für lange Ausdrücke, die mehrfach verwendet werden -->
@let address = order().customer.billing.address;
<p>{{ address.street }}, {{ address.city }}, {{ address.zip }}</p>
<!-- und innerhalb eines @for, pro Element -->
@for (p of products(); track p.id) {
@let finalPrice = p.discount ? p.price * (1 - p.discount) : p.price;
<span>{{ finalPrice | currency: 'RON' }}</span>
}4 von 5 Produkten · Summe 2900 RON
- Maus150 RON
- Tastatur-20%360 RON
- Kopfhörer900 RON
- Monitor-10%1260 RON
Die neuen Ausdrücke im Überblick
<!-- Template literals (v20) -->
<p>{{ `Hallo, ${user().name}! Du hast ${count()} Nachrichten.` }}</p>
<img [alt]="`Avatar von ${user().name}`" />
<!-- Operatoren: ** (Potenz), in, typeof, void (v20) -->
{{ 2 ** 10 }}
@if ('discount' in product) { ... }
@if (typeof value === 'string') { ... }
<button (click)="void save()">Speichern</button>
<!-- Reguläre Ausdrücke (v21.0) -->
@let valid = /^07[0-9]{8}$/.test(phone());
<!-- Spread in Arrays, Objekten und Argumenten (v21.1) -->
@let all = [...defaults, ...extra()];
<app-chart [options]="{ ...baseOptions, color: accent() }" />
{{ max(...values()) }}
<!-- Arrow functions (v21.2) -->
@let done = tasks().filter((t) => t.done);
<app-list [sortFn]="(a, b) => a.name.localeCompare(b.name)" />
<!-- instanceof (v21.2) — die Klasse muss aus der Komponente bereitgestellt werden -->
@if (error() instanceof HttpErrorResponse) { <p>HTTP-Fehler {{ error().status }}</p> }instanceof setzt voraus, dass die Klasse bereitgestellt wird
Das Template sieht nur die Member der Komponente (keine globalen Variablen oder Imports). Eine Klasse, die mit instanceof verwendet wird, muss daher in einem Feld bereitgestellt werden.
import { HttpErrorResponse } from '@angular/common/http';
@Component({
template: `
@if (error() instanceof HttpErrorResponse) {
<p>Der Server hat mit {{ error().status }} geantwortet</p>
} @else if (error()) {
<p>Unerwarteter Fehler</p>
}
`,
})
export class ErrorView {
readonly error = input<unknown>();
// das Template sieht nur die Member der Komponente, also muss die Klasse bereitgestellt werden:
protected readonly HttpErrorResponse = HttpErrorResponse;
}Kommentare innerhalb von Tags v22
<!-- v22: Kommentare innerhalb von Tags -->
<button
// TODO: durch die Tooltip-Direktive ersetzen
class="btn"
/* vorübergehend deaktiviert:
[disabled]="saving()" */
(click)="save()"
>
Speichern
</button>Wichtige Änderung in v22: ?. liefert undefined
Zur Angleichung an TypeScript liefert Optional Chaining in Templates jetzt undefined (nicht mehr null). Die ng update-Migration deaktiviert vorübergehend die Diagnosen nullishCoalescingNotNullable und optionalChainNotNullable, damit dein Build nicht fehlschlägt; aktiviere sie wieder, nachdem du die Templates geprüft hast.
<!-- v22 (Breaking): ?. in Templates liefert undefined, wie in TypeScript (vorher: null) -->
{{ user()?.name }} <!-- user null → undefined -->
<!-- ✗ strikte Vergleiche mit null ändern sich -->
@if (user()?.name === null) { ... } <!-- jetzt nie mehr true -->
<!-- ✓ mit == null vergleichen oder ?? verwenden -->
@if (user()?.name == null) { ... }
{{ user()?.name ?? 'Anonym' }}Ab v22.2 lässt der Compiler das Template auch private-Member lesen. Die Empfehlung des Styleguides bleibt protected für alles, was das Template nutzt: So ist klar, dass der Member für die View gedacht ist und keine öffentliche API darstellt.
Wie viel Logik gehört ins Template?
// Arrow Functions und Methoden in Templates laufen bei JEDER Change Detection der Komponente.
// Bei großen Listen oder aufwendigen Berechnungen verschiebst du die Logik in ein computed():
// statt: @let done = tasks().filter((t) => t.done);
readonly done = computed(() => this.tasks().filter((t) => t.done)); // memoisiert
// Regel: Das Template IST der Ort für einfache, lokale Ausdrücke.
// Geschäftslogik, aufwendige Sortierungen und alles Wiederverwendete → in die Klasse (computed) oder eine Pipe.- Kurze Formatierung, ein Template Literal, eine einfache Bedingung.
@let, um einen mehrfach verwendeten Teilausdruck zu benennen.- Kleine Filter auf kurzen Listen, lokal zur View.
- Geschäftsregeln („Darf der Benutzer bearbeiten?“).
- Aufwendige Berechnungen oder große Listen →
computed()(mit Memoization). - Transformationen, die in vielen Komponenten wiederverwendet werden → eine Pipe.
- Alles, was du per Unit-Test prüfen willst.
Häufige Stolperfallen
@letkann nicht neu zugewiesen werden und ist vor seiner Deklaration nicht sichtbar.- Globale Variablen (
Math,JSON,Date, importierte Klassen) sind im Template nicht sichtbar — stelle sie aus der Komponente bereit. {{ { ...obj } }}: Ein Objektliteral innerhalb einer Interpolation kollidiert mit den geschweiften Klammern; nutze es in einem Property Binding oder in@let.