Templates sind das Bindeglied zwischen Komponenten-Logik und DOM. Angular bietet dafür eine kompakte, aber sehr ausdrucksstarke Syntax: doppelte geschweifte Klammern für Text, eckige Klammern für Properties, runde Klammern für Events. Dieser Artikel beleuchtet die ersten beiden Welten — die Daten-Bindings — im Detail, mit allen Schreibweisen, Fallstricken und Performance-Hinweisen. Stand ist Angular 21 (stable, Nov 2025); markante Erweiterungen aus v17–v21 sind im Text vermerkt. Die gezeigten Beispiele sind Standalone, lassen sich aber 1:1 auch in NgModule-Codebasen einsetzen.
Was ist Template-Binding?
Ein Template-Binding ist ein deklarativer Mechanismus, der einen Wert aus der Komponenten-Klasse auf eine Stelle im DOM abbildet — entweder als sichtbaren Text, als DOM-Property, als CSS-Klasse, als Style-Eigenschaft oder als HTML-Attribut. Angular unterscheidet drei grundlegende Richtungen: Interpolation und Property-Binding fließen vom Component zum DOM (one-way down), Event-Binding fließt zurück (one-way up), und Two-Way-Binding kombiniert beide.
Dabei ist eine Unterscheidung zentral: HTML-Attribute und DOM-Properties sind nicht dasselbe. Attribute sind die Zeichenketten, die im Quelltext stehen (<input value="hi">); Properties sind die Werte, die das DOM-Objekt zur Laufzeit hält (inputElement.value). Der Browser initialisiert viele Properties einmalig aus den Attributen — danach driften sie auseinander. Property-Binding sprich die Property an, Attribute-Binding gezielt das Attribut.
| Syntax | Was es macht | Beispiel |
|---|---|---|
{{ expr }} | Interpolation: rendert als Textknoten | {{ user.name }} |
[prop]="expr" | Property-Binding: setzt DOM-Property | [disabled]="!form.valid" |
[attr.name]="expr" | Attribute-Binding: setzt HTML-Attribut | [attr.aria-label]="label" |
[class.x]="bool" | Klasse bedingt setzen | [class.active]="isActive" |
[class]="exprOrObj" | Klassen aus String/Array/Objekt | [class]="{ active: x, big: y }" |
[style.prop]="value" | Einzelne Style-Property setzen | [style.color]="'tomato'" |
[style.prop.unit]="num" | Style mit Unit-Suffix | [style.padding.px]="16" |
(event)="stmt" | Event-Binding (siehe Folgeartikel) | (click)="save()" |
[(prop)]="ref" | Two-Way-Binding (siehe Folgeartikel) | [(ngModel)]="value" |
Interpolation mit {{ … }}
Interpolation ist der einfachste Weg, dynamische Werte als Text in eine Komponente zu rendern. Die doppelten geschweiften Klammern markieren einen Bereich, in dem Angular den Ausdruck auswertet, das Ergebnis in einen String konvertiert und als Text-Knoten einfügt. Das Resultat ist immer Text — niemals interpretiertes HTML. Damit ist die Schreibweise per Default sicher gegen XSS: ein bösartiger HTML-String landet als angezeigter String, nicht als ausgeführtes Markup.
Erlaubt sind Property-Zugriffe, Methodenaufrufe, Operatoren, Pipes und Signal-Aufrufe. null und undefined werden zu einem leeren String — das spart in vielen Fällen explizite Null-Checks. Zahlen, Booleans und Objekte werden via toString() konvertiert (was bei Plain-Objects das wenig hilfreiche [object Object] ergibt — daher gehört für strukturierte Daten ein passendes JSON.stringify oder eine Pipe davor).
import { Component, signal, computed } from '@angular/core';
interface User {
firstName: string;
lastName: string;
email: string;
age: number;
}
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<article class="card">
<h2>{{ user().firstName }} {{ user().lastName }}</h2>
<p>Alter: {{ user().age }} Jahre</p>
<p>Initialen: {{ initials() }}</p>
<p>E-Mail: {{ user().email | lowercase }}</p>
<p>Status: {{ user().age >= 18 ? 'volljährig' : 'minderjährig' }}</p>
<p>Ungültiger Wert: {{ undefinedValue }} (rendert leer)</p>
</article>
`,
})
export class UserCardComponent {
user = signal<User>({
firstName: 'Max',
lastName: 'Mustermann',
email: 'Max@Example.COM',
age: 28,
});
undefinedValue: string | undefined = undefined;
// Computed-Signal: wird automatisch aktualisiert, wenn user() sich ändert
initials = computed(() =>
`${this.user().firstName[0]}${this.user().lastName[0]}`.toUpperCase()
);
}Konzeptuell ist {{ expr }} ein syntaktischer Zucker für ein Property-Binding auf den Text-Inhalt. Beide Schreibweisen erzeugen identische Ausgabe — Interpolation ist nur kürzer und konventioneller für reinen Text.
Property-Binding mit [property]
Property-Binding setzt eine DOM-Property zur Laufzeit. Das ist nicht nur kürzer als Interpolation in Attributen, sondern semantisch korrekter: [disabled]="false" schaltet den Button wirklich aus dem Disabled-Zustand frei, während disabled="false" als String-Attribut den Button trotzdem disabled lässt — denn HTML wertet die bloße Existenz des Attributs als Wahrheit, unabhängig vom Inhalt.
Angulars Template-Compiler prüft Property-Bindings typsicher gegen die Komponenten-Definition. Ein Tippfehler im Property-Namen ([disabledd]) wird zur Build-Zeit als Fehler gefangen — nicht erst zur Laufzeit. Diese Prüfung greift sowohl bei nativen DOM-Elementen (gegen die TypeScript-DOM-Lib) als auch bei eigenen Components (gegen deren input()-Deklarationen).
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-form-row',
standalone: true,
template: `
<label for="email">E-Mail</label>
<!-- Property-Binding: typsicher gegen HTMLInputElement -->
<input
id="email"
type="email"
[value]="email()"
[disabled]="isLocked()"
[readOnly]="isReviewMode()"
[placeholder]="placeholder"
/>
<!-- Falle: das hier disabled IMMER, weil das Attribut existiert -->
<button disabled="false">Falsch — immer disabled</button>
<!-- Korrekt: Property-Binding mit Boolean-Expression -->
<button [disabled]="!email()">Senden</button>
<!-- Bilder: dynamische src vermeidet 404 vor Initialisierung -->
<img [src]="avatarUrl()" [alt]="'Avatar von ' + email()" />
<!-- Links: hreflang ist eine echte DOM-Property -->
<a [href]="docsUrl" [hreflang]="lang">Dokumentation</a>
`,
})
export class FormRowComponent {
email = signal('');
isLocked = signal(false);
isReviewMode = signal(false);
placeholder = 'name@firma.de';
avatarUrl = signal('/img/placeholder.svg');
docsUrl = 'https://example.com/docs';
lang = 'de';
}Class- und Style-Bindings
Klassen und Stile gehören zu den am häufigsten dynamisch gebundenen DOM-Eigenschaften. Angular bietet dafür mehrere Schreibweisen, die sich in Granularität und Lesbarkeit unterscheiden — und die sich kombinieren lassen, ohne sich gegenseitig auszuhebeln.
Class-Binding in drei Geschmacksrichtungen
Die Single-Class-Form [class.name]="bool" ist ideal, wenn genau eine Klasse abhängig von einer Bedingung gesetzt werden soll. Die String-Form [class]="someString" ersetzt die gesamte (von Angular verwalteten) Klassenliste. Die Object-Form [class]="{ a: x, b: y }" ist der Vielzweck-Variante: pro Schlüssel eine Klasse, der zugehörige Boolean entscheidet über das Setzen.
<!-- Single-Class: kompakt für eine einzelne Bedingung -->
<button [class.active]="isActive()">Toggle</button>
<!-- String-Form: mehrere Klassen aus einem berechneten Wert -->
<div [class]="cssClasses()">…</div>
<!-- Object-Form: deklaratives Mapping -->
<article [class]="{
card: true,
'card--featured': isFeatured(),
'card--archived': isArchived(),
'card--unread': unreadCount() > 0
}">…</article>
<!-- Array-Form: Liste von Klassen -->
<span [class]="['tag', priorityClass()]">…</span>
<!-- Kombiniert: bewährte Praxis -->
<li
class="item"
[class.item--selected]="isSelected()"
[class.item--disabled]="isDisabled()"
>…</li>Style-Binding und das Unit-Suffix
Bei Stilen lohnt sich die Variante mit Unit-Suffix: [style.width.px]="120" ist sauberer als die Concat-Lösung [style.width]="value + 'px'" und vermeidet Tippfehler. Unterstützt sind alle gängigen CSS-Längeneinheiten (px, em, rem, %, vh, vw, etc.) sowie Zeit (ms, s).
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-padding-slider',
standalone: true,
template: `
<input
type="range"
min="0"
max="64"
[value]="padding()"
(input)="padding.set(+($event.target as HTMLInputElement).value)"
/>
<div
class="preview"
[style.padding.px]="padding()"
[style.background-color]="bgColor()"
[style.border-radius.rem]="0.5"
[style.transition-duration.ms]="200"
>
Padding: {{ padding() }}px
</div>
`,
styles: [`.preview { background: #eee; }`],
})
export class PaddingSliderComponent {
padding = signal(16);
bgColor = signal('#f0f9ff');
}Bei Konflikten greift eine klare Reihenfolge: spezifischere Bindings (Single-Class, Single-Style mit Unit) gewinnen gegen generische ([class]-Object, [style]-Object). Das erlaubt eine Basis-Konfiguration über die Object-Form mit gezielten Überschreibungen über Single-Bindings — ein verbreitetes Pattern für Theme-Komponenten.
Attribute-Binding mit attr.
Nicht jedes HTML-Attribut hat eine zugehörige DOM-Property. ARIA-Attribute (aria-label, aria-pressed, role), Tabellen-Attribute (colspan, rowspan) und SVG-Attribute (cx, cy, r) sind die typischen Kandidaten. Für sie funktioniert reines Property-Binding nicht — der Compiler meldet Can't bind to ... since it isn't a known property.
Die Lösung ist das Attribute-Binding mit dem Präfix attr.. Es ruft unter der Haube setAttribute() auf — und bei einem null/undefined-Wert das passende removeAttribute(). Damit lässt sich ein Attribut auch wieder gezielt entfernen.
<!-- ARIA: aria-label hat keine DOM-Property -->
<button
type="button"
[attr.aria-label]="closeLabel"
[attr.aria-pressed]="isPressed()"
(click)="toggle()"
>×</button>
<!-- Tabellen: colspan/rowspan -->
<table>
<tr>
<td [attr.colspan]="span()">Mehrere Spalten</td>
</tr>
</table>
<!-- SVG: in SVG sind viele Attribute keine Properties -->
<svg width="100" height="100">
<circle
[attr.cx]="x()"
[attr.cy]="y()"
[attr.r]="radius()"
fill="currentColor"
/>
</svg>
<!-- data-Attribute: für Test-IDs oder Plain-Hooks -->
<div [attr.data-testid]="testId" [attr.data-state]="state()">…</div>
<!-- Wert null entfernt das Attribut komplett -->
<input [attr.required]="isRequired() ? '' : null" />Safe Navigation und Optional Chaining
Templates müssen oft mit Daten umgehen, die noch nicht da sind — der HTTP-Request ist gerade unterwegs, der Router-Resolver hat noch nicht aufgelöst, der Parent reicht erst beim nächsten Tick einen Wert herunter. Statt das ganze Template hinter einem @if zu verstecken, lässt sich der Zugriff defensiv gestalten.
Der Safe-Navigation-Operator ?. funktioniert in Templates wie in JavaScript — mit einem feinen Unterschied: in Angular liefert null?.x den Wert null (statt undefined), was bei nachgelagerten Vergleichen oft konsistenter ist. Ergänzend funktionieren der Nullish-Coalescing-Operator ?? und (mit Einschränkungen) der Non-Null-Assertion-Operator !.
<!-- Sicherer Zugriff auf verschachtelte Optional-Properties -->
<p>Stadt: {{ user()?.address?.city }}</p>
<!-- Mit Fallback per Nullish-Coalescing -->
<p>Land: {{ user()?.address?.country ?? 'unbekannt' }}</p>
<!-- Methodenaufruf nur, wenn Objekt existiert -->
<p>Anzeigename: {{ user()?.getDisplayName() ?? '–' }}</p>
<!-- Array-Zugriff -->
<p>Erste Rolle: {{ user()?.roles?.[0] ?? 'keine' }}</p>
<!-- $any als Escape-Hatch bei TS-Type-Issues -->
<p>{{ $any(legacyData).deepProp }}</p>$any(value) ist ein Cast nach any, der nur in Templates existiert. Er ist die Notbremse, wenn der Template-Type-Checker zu strikt ist (etwa bei dynamisch geformten Daten aus einer unknown-API). Als Stil-Marker behandelt: $any zeigt eine Schmerzstelle, die idealerweise durch ein sauberes Typing ersetzt wird, sobald die Datenstruktur klar ist.
Template-Expressions: was geht, was nicht
Template-Expressions sind ein bewusst eingeschränkter Subset von JavaScript. Die Einschränkung ist kein Versehen, sondern Designprinzip: Templates sollen lesbar bleiben und keine Side-Effects produzieren. Was rein lesend ist, ist erlaubt — was schreibt oder die Programmstruktur verändert, ist verboten.
| Erlaubt | Verboten |
|---|---|
Property-Zugriff (a.b.c) | Zuweisungen (x = 1) |
Methoden-Aufruf (x.do()) | Increment/Decrement (++, --) |
Arithmetik (+, -, *, /, %) | new-Operator |
Vergleiche (===, <, >=) | delete, void, typeof (Statement) |
Logische Operatoren (&&, ||, !) | Bitwise (&, |, ^, <<) |
Ternary (a ? b : c) | Variablen-/Funktions-Deklarationen |
Optional Chaining (?.) | Destructuring |
Nullish Coalescing (??) | Block-Statements ({ ... }) |
Pipes (x | date) | Pipes in Event-Handlern |
instanceof (seit v21) | BigInt-Literale |
| Template-Literals | Globals wie Number, parseInt |
Event-Handler sind Statements (nicht Expressions) und dürfen daher Zuweisungen enthalten — aber keine Pipes. Diese feine Trennung erklärt, warum (click)="count = count + 1" funktioniert, {{ count = count + 1 }} aber nicht.
Mit Angular 21 sind zwei kleine, aber wichtige Erweiterungen dazugekommen: instanceof funktioniert in Templates (nützlich für Discriminated-Unions ohne Helper-Methoden) und der Template-Type-Checker beherrscht exhaustive Switch im @switch-Block — fehlt ein Case, meckert der Compiler.
<!-- v21: instanceof in Templates -->
@if (event instanceof ClickEvent) {
<p>Klick-Event mit Position {{ event.x }}, {{ event.y }}</p>
} @else if (event instanceof KeyEvent) {
<p>Tastatur-Event mit Key {{ event.key }}</p>
}
<!-- Pipes in Expressions (erlaubt) -->
<p>{{ user().createdAt | date:'shortDate' }}</p>
<!-- Ternary mit Pipe -->
<p>{{ amount > 0 ? (amount | currency:'EUR') : 'kein Betrag' }}</p>
<!-- Verboten: würde Compile-Error werfen
<p>{{ x = 5 }}</p>
<p>{{ ++count }}</p>
<p>{{ new Date() }}</p>
-->Performance-Pitfalls bei Methods im Template
Eine Template-Expression wird bei jedem Change-Detection-Cycle neu ausgewertet. Bei einem Property-Zugriff ist das billig, bei einem Methodenaufruf kann es teuer werden — und bei einer nicht-deterministischen Methode wie Date.now() führt es zu inkonsistentem Rendering, weil der Wert sich zwischen zwei Aufrufen ändert.
Drei Strategien helfen:
computed()für abgeleitete Werte aus Signals — wird gecached, läuft nur bei tatsächlicher Abhängigkeitsänderung.- Pure Pipes für Transformationen mit Identitäts-basiertem Caching — bei gleichem Input wird das gleiche Output ohne erneute Berechnung wiederverwendet.
OnPush-Change-Detection in der Komponente, kombiniert mit Signals — Angular rendert nur, wenn ein Signal in dieser Komponente sich ändert.
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
interface Item { name: string; price: number; qty: number; }
@Component({
selector: 'app-cart',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ul>
@for (item of items(); track item.name) {
<li>{{ item.name }} – {{ item.price * item.qty | currency:'EUR' }}</li>
}
</ul>
<!-- GUT: computed wird nur neu berechnet, wenn items() sich ändert -->
<p>Summe: {{ total() | currency:'EUR' }}</p>
<!-- SCHLECHT: läuft bei jedem CD-Cycle -->
<!-- <p>Summe: {{ calculateTotal() | currency:'EUR' }}</p> -->
`,
})
export class CartComponent {
items = signal<Item[]>([]);
// Memoisiert: nur bei items()-Änderung neu ausgeführt
total = computed(() =>
this.items().reduce((sum, i) => sum + i.price * i.qty, 0)
);
// Anti-Pattern für Templates:
calculateTotal(): number {
return this.items().reduce((sum, i) => sum + i.price * i.qty, 0);
}
}Notizen aus der Praxis
Interpolation ist syntaktischer Zucker
{{ expr }} entspricht intern einem Property-Binding auf den Text-Inhalt. Beide Schreibweisen rendern als reiner Text, niemals als HTML — das ist die Default-Sicherheit gegen XSS, die Angular ohne weitere Konfiguration mitbringt.
Property-Binding ist typsicher zur Build-Zeit
Der Template-Compiler prüft Property-Namen gegen die TypeScript-Typen des Ziels — sowohl bei nativen DOM-Elementen als auch bei eigenen Komponenten. Ein Tippfehler in [disabledd] wird nicht erst beim Klick zur Laufzeit auffallen, sondern direkt beim ng build.
innerHTML umgeht Sanitization NICHT
Auch wenn der Name das suggeriert: [innerHTML]=“userInput” rendert nicht roh. Angulars DomSanitizer entfernt gefährliche Tags (<script>, Inline-Event-Handler). Wer wirklich vertrauenswürdiges HTML einfügen muss, nutzt explizit bypassSecurityTrustHtml() — und übernimmt damit die Verantwortung für die Quelle.
SVG braucht Attribute-Binding
In HTML5 sind viele Werte zugleich Attribut und Property — in SVG ist das anders. cx, cy, r, d, points existieren nur als Attribute. Property-Binding scheitert hier mit Can’t bind to ‘cx’; Attribute-Binding mit [attr.cx] funktioniert.
instanceof und Exhaustive-Switch in v21
Angular 21 hat den Template-Type-Checker spürbar erweitert. instanceof in Templates erlaubt sauberes Discriminated-Union-Handling ohne Helper-Methoden im Component, und der @switch-Block erkennt fehlende Cases bei String- oder Enum-Literal-Typen.
Template-Methoden sind auch unter OnPush riskant
OnPush reduziert die Häufigkeit der Change-Detection — verhindert sie aber nicht. Eine im Template aufgerufene Methode läuft pro CD-Zyklus mindestens einmal. Bei nicht-deterministischen Methoden (Date.now(), Math.random()) entstehen Inkonsistenzen pro Rendering. Lieber computed() oder eine Pure Pipe.
$any als Last-Resort markieren
$any(value).x ist die Notbremse für Template-Type-Errors, die sich nicht anders lösen lassen. Es deaktiviert die Type-Prüfung punktuell. Praktisch — aber als Schmerzstelle behandeln: jeder $any-Aufruf ist ein TODO, das auf besseres Typing wartet, sobald die Datenstruktur klar ist.
Weiterführende Ressourcen
Externe Quellen
- Binding overview – Angular.dev
- Property Binding – Angular.dev
- Expression Syntax – Angular.dev
- DomSanitizer API – Angular.dev