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.

SyntaxWas es machtBeispiel
{{ 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).

TypeScript user-card.component.ts
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).

TypeScript form-row.component.ts
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.

HTML class-bindings.component.html
<!-- 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).

TypeScript padding-slider.component.ts
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.

HTML aria-bindings.component.html
<!-- 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 !.

HTML safe-navigation.component.html
<!-- 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.

ErlaubtVerboten
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-LiteralsGlobals 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.

HTML expressions.component.html
<!-- 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:

  1. computed() für abgeleitete Werte aus Signals — wird gecached, läuft nur bei tatsächlicher Abhängigkeitsänderung.
  2. Pure Pipes für Transformationen mit Identitäts-basiertem Caching — bei gleichem Input wird das gleiche Output ohne erneute Berechnung wiederverwendet.
  3. OnPush-Change-Detection in der Komponente, kombiniert mit Signals — Angular rendert nur, wenn ein Signal in dieser Komponente sich ändert.
TypeScript optimized-cart.component.ts
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

/ Weiter

Zurück zu Templates & Control Flow

Zur Übersicht