Moderne Webanwendungen bestehen aus tief verschachtelten Komponenten-Bäumen. Oftmals muss ein übergeordneter Container (Parent) Daten an seine isolierten Kind-Elemente (Child) weiterreichen – sei es ein Benutzerobjekt, ein Konfigurations-String oder ein Farbschema.
In Angular nennt man diese Schnittstellen “Inputs”. Mit Version 17.1 hat Angular sein Input-System durch die Einführung von Signal-based Inputs revolutioniert. In diesem Artikel tauchen wir tief in die Mechanik von input(), Datentransformationen, Zwei-Wege-Bindung mit model() und klassische @Input() Fallstricke ein.
Architektur: Signal-based Inputs (Die moderne Art)
Der fundamentale Wechsel in neueren Angular-Versionen besteht darin, dass Inputs nicht mehr als einfache Klassen-Eigenschaften definiert werden, in die Angular “magisch” Werte hineinschreibt, sondern als reaktive Signals.
Die Funktion input() gibt ein schreibgeschütztes InputSignal zurück. Das bedeutet:
- Das Child kann den Wert extrem performant auslesen.
- Wenn der Parent neue Daten schickt, benachrichtigt das Signal das Template vollautomatisch (Zoneless Change Detection).
- Das Child kann den erhaltenen Wert nicht überschreiben. Inputs sind streng One-Way (für Two-Way gibt es
model()).
import { Component, input, computed, effect } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="card">
<!-- Auslesen des Signals im Template zwingend durch den Funktionsaufruf () -->
<h2>{{ userName() }}</h2>
<p>{{ greeting() }}</p>
</div>
`,
standalone: true
})
export class UserCardComponent {
// Optionaler Input mit Standardwert. Der Typ (string) wird automatisch abgeleitet.
userName = input('Unbekannter Gast');
// Abgeleiteter Zustand (Computed Signal).
// Er aktualisiert sich vollautomatisch und cached das Ergebnis,
// bis sich userName das nächste Mal ändert.
greeting = computed(() => `Hallo, ${this.userName()}!`);
constructor() {
// Das moderne Äquivalent zu ngOnChanges: Ein effect.
// Er feuert automatisch, sobald sich einer der Signal-Werte darin ändert.
effect(() => {
console.log('Der Name ist jetzt:', this.userName());
});
}
}Zwingend erforderliche Inputs (input.required)
Oftmals ergibt eine Komponente absolut keinen Sinn, wenn ihr keine Daten übergeben werden. Ein UserAvatar ohne Bild-URL ist nutzlos. Ein Standardwert wie ein leerer String verdeckt oft nur Bugs.
Mit input.required<Typ>() erzwingst du, dass ein Wert vom Parent geliefert werden muss. Vergisst der Entwickler das Binding im Parent-HTML, bricht der Angular-Compiler den Build sofort mit einem fatalen Fehler ab!
export class UserAvatarComponent {
// Es gibt keinen Startwert. Der Compiler schützt uns vor Fehlern.
imageUrl = input.required<string>();
}Binding im Parent-Template
Egal ob du alte oder neue Input-Techniken im Child nutzt, das Binding im HTML der Parent-Komponente sieht immer exakt gleich aus.
Angular unterscheidet zwischen Property Binding (mit eckigen Klammern []) und String Binding (ohne Klammern).
<!-- Property Binding: 'currentUser.avatarUrl' wird als TypeScript-Code evaluiert -->
<app-user-avatar [imageUrl]="currentUser.avatarUrl" />
<!-- String Binding: Der Text "Max Mustermann" wird als statischer String übergeben -->
<app-user-card userName="Max Mustermann" />
<!-- ACHTUNG: Willst du eine Zahl (oder Boolean) statisch übergeben, BRAUCHST du Klammern,
ansonsten denkt Angular, es ist der Text "28". -->
<app-user-card [age]="28" [isAdmin]="true" />Input Transforms (Daten bei Ankunft umwandeln)
Ein oft übersehenes, aber extrem mächtiges Feature sind Transforms. Manchmal sendet der Parent Daten in einem ungünstigen Format (z.B. als reinen String aus einem HTML-Attribut), aber deine TypeScript-Klasse erwartet einen sauberen Boolean.
Angular stellt eingebaute Konverter wie booleanAttribute zur Verfügung. Ein Attribut wie <button disabled> übergibt technisch gesehen einen leeren String "". Der Transform macht daraus intern true.
import { Component, input, booleanAttribute } from '@angular/core';
@Component({
selector: 'app-button',
template: `<button [disabled]="disabled()">Klick</button>`,
standalone: true
})
export class ButtonComponent {
// Wandelt den Input VOR dem Speichern im Signal automatisch in einen echten Boolean um.
disabled = input(false, { transform: booleanAttribute });
}Eigene Custom Transforms
Du kannst jede beliebige Funktion als Transform übergeben. Nützlich, um Arrays zu sortieren, Whitespaces abzuschneiden oder IDs aus Objekten zu extrahieren.
function trimText(val: string | undefined): string {
return val ? val.trim() : '';
}
// ... in der Klasse
headline = input('', { transform: trimText });Zwei-Wege-Bindung mit model()
Wenn die Child-Komponente Daten empfangen, aber auch verändern und synchron zum Parent zurückschicken soll (z.B. ein Schalter, Input-Feld oder Akkordeon), benötigst du Two-Way Data Binding ([(ngModel)] Prinzip).
Die moderne model() Funktion erstellt intern ein WritableSignal kombiniert mit einem versteckten Output-Event.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-counter',
template: `<button (click)="add()">{{ count() }}</button>`,
standalone: true
})
export class CounterComponent {
// Startwert ist 0.
count = model(0);
add() {
// update() schreibt den neuen Wert in das Signal UND feuert vollautomatisch
// das Event 'countChange' nach oben an den Parent.
this.count.update(c => c + 1);
}
}Im Parent nutzt du die “Banana-in-a-box”-Syntax: <app-counter [(count)]="myVariable" />. Ändert der Nutzer den Counter im Child, springt myVariable im Parent magisch auf denselben Wert.
Stolperfallen (Alt und Neu)
Verwirrung durch Funktionsklammern
Da die neuen input() und model() Eigenschaften Signale zurückgeben, musst du im Template beim Auslesen zwingend die runden Klammern nutzen: {{ userName() }}. Lässt du sie weg, versucht Angular, das Signal-Objekt selbst zu rendern, was meistens mit dem Fehler [object Object] oder einem leeren Feld endet.
ngOnChanges vs. effect()
Wer vom klassischen @Input() Dekorator kommt, ist es gewohnt, auf Prop-Änderungen extrem umständlich mit dem Lifecycle-Hook ngOnChanges und dem SimpleChanges Objekt zu reagieren. Mit Signals nutzt man dafür computed() (wenn man Daten ableiten will) oder effect() (für Seiteneffekte wie API-Aufrufe). Vermische beide Welten niemals!
Objekte als Input direkt mutieren
Ein klassischer Anfängerfehler: Du übergibst ein Objekt [user]=“myUser” an ein Child. Das Child ändert direkt user().name = “Test”. Das zerstört den “Unidirectional Data Flow” massiv. Ein Child sollte niemals die Objekte des Parents direkt manipulieren, auch wenn JavaScript das zulässt! Nutze dafür zwingend Outputs oder Models.