Eine Komponente kommuniziert mit ihrem Parent über Inputs (Daten, die hereinkommen) und Outputs (Events, die hinausgehen). Die klassische Regel gilt weiterhin: Data down, Events up. Geändert hat sich die API: input() und output() sind Funktionen, und Inputs sind Signals.
Ein vollständiges Beispiel
import { Component, booleanAttribute, computed, input, numberAttribute, output } from '@angular/core';
@Component({
selector: 'app-star-rating',
template: `
@for (star of stars(); track star) {
<button [class.on]="star <= value()" [disabled]="readonly()" (click)="rate.emit(star)">★</button>
}
`,
})
export class StarRating {
readonly value = input.required<number>(); // Pflicht
readonly max = input(5, { transform: numberAttribute }); // max="10" → 10
readonly readonly = input(false, { transform: booleanAttribute }); // <app-star-rating readonly />
readonly rate = output<number>();
// Inputs sind Signals → direkt mit computed ableiten, kein ngOnChanges
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}- Gib eine Bewertung ab — der Parent erhält das Event (rate).
Optionen: required, transform, alias
| Schreibweise | Typ | Wann |
|---|---|---|
input(5) | InputSignal<number> | Hat einen sinnvollen Standardwert. |
input<string>() | InputSignal<string | undefined> | Wirklich optional. |
input.required<User>() | InputSignal<User> | Die Komponente ergibt ohne ihn keinen Sinn. |
transform: booleanAttribute | akzeptiert '', 'true' | Flags: <app-x disabled />. |
transform: numberAttribute | akzeptiert '10' | Zahlen, die als statische Attribute geschrieben werden. |
// Alias (der Name im Template weicht vom Namen in der Klasse ab) — selten verwendet
readonly label = input('', { alias: 'ariaLabel' });
// Eigener Transform: den Wert beim Eingang normalisieren
readonly tags = input([], {
transform: (value: string | string[]) => (Array.isArray(value) ? value : value.split(',')),
});
// Optionaler Input ohne Standardwert → der Typ enthält undefined
readonly hint = input<string>(); // InputSignal<string | undefined>
// Output mit Alias
readonly closed = output<void>({ alias: 'close' });
this.closed.emit();Früher und heute
// ✓ Die moderne API: Signals + Funktionen
export class StarRating {
readonly value = input.required<number>();
readonly max = input(5, { transform: numberAttribute });
readonly rate = output<number>();
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}// ✗ Die alte API, basierend auf Dekoratoren
export class StarRating implements OnChanges {
@Input({ required: true }) value!: number;
@Input({ transform: numberAttribute }) max = 5;
@Output() rate = new EventEmitter<number>();
stars: number[] = [];
ngOnChanges(changes: SimpleChanges) {
if (changes['max']) {
this.stars = Array.from({ length: this.max }, (_, i) => i + 1);
}
}
}Automatische Migration: ng generate @angular/core:signal-input-migration und ng generate @angular/core:outputs.
Inputs sind schreibgeschützt
export class Pager {
readonly page = input(1);
next() {
// ✗ Inputs sind in der Komponente schreibgeschützt
// this.page.set(this.page() + 1);
// ✓ Möglichkeiten:
// - ein Event emittieren und den Parent entscheiden lassen: this.pageChange.emit(this.page() + 1)
// - model() verwenden, wenn es ein Two-Way-Wert ist → siehe Seite model()
// - linkedSignal(() => this.page()) für eine lokale Kopie verwenden
}
}Outputs programmatisch abonnieren
// Du kannst einen Output programmatisch abonnieren (z. B. bei einer dynamisch erzeugten Komponente)
const ref = viewContainer.createComponent(StarRating);
const sub = ref.instance.rate.subscribe((value) => console.log(value));
// sub.unsubscribe(); — passiert automatisch, wenn die Komponente zerstört wird
// Oder einfacher, mit Bindings bei der Erzeugung (siehe „Dynamische Komponenten“):
viewContainer.createComponent(StarRating, {
bindings: [inputBinding('value', this.rating), outputBinding('rate', (v) => this.save(v))],
});- Der Compiler wirft einen Fehler, wenn du denselben Input/Output zweimal an einem Element bindest.
data-*-Attribute binden keine Inputs oder Outputs mehr (z. B. erreichtdata-valueden Inputvaluenicht mehr).- v22.2: Die Option
strictUnclaimedEventNameserkennt falsch geschriebene Outputs ((rated)statt(rate)).
Drei Ebenen tiefer: Prop Drilling
Inputs und Outputs verbinden einen Elternteil mit seinem direkten Kind. Liegen die Daten oben und werden drei Ebenen tiefer gebraucht, muss jede Komponente dazwischen ein Input deklarieren, um sie weiterzugeben, und ein Output, um die Events wieder nach oben zu schicken — obwohl sich keine davon für die Daten interessiert.
signal: countinput: countinput: countoutput: addedoutput: addedoutput: addedoutput: added- Deklarierte Inputs
- 2
- Deklarierte Outputs
- 4
- Komponenten, die den Warenkorb kennen
- 7
- Drücke In den Warenkorb.
Mit Inputs und Outputs gibt es ProductCard, ProductGrid und ShopPage nur, um das Event weiterzureichen, und jede ist eine weitere Stelle, die kaputtgehen kann, wenn sich das Event ändert. Mit einem Store wissen nur die beiden Komponenten vom Warenkorb, die ihn wirklich nutzen. Behalte Inputs und Outputs für die direkte Beziehung — sie machen eine Komponente wiederverwendbar und ihren Vertrag sichtbar — und nimm einen Service, wenn die Daten Ebenen überspringen oder ein Geschwister erreichen müssen.
// jede Ebene dazwischen deklariert ein Output nur, um den Klick weiterzugeben
@Component({
selector: 'app-product-grid',
template: `<app-product-card (added)="added.emit()" />`,
})
export class ProductGrid {
readonly added = output();
}
// ProductCard, ShopPage: dieselben drei Zeilen, für jede noch einmal
// …und der Zähler wandert über den anderen Zweig durch weitere Inputs nach unten
@Component({
template: `
<app-header [count]="count()" />
<app-shop-page (added)="count.update((n) => n + 1)" />
`,
})
export class Shop {
protected readonly count = signal(0);
}Wann ja, wann nein
- Direkte Kommunikation Parent ↔ Child.
- Dumb-Komponenten / wiederverwendbare UI (Buttons, Cards, Tabellen).
- Events mit Bedeutung:
(save),(delete),(select).
- Ein Wert, der in beide Richtungen synchronisiert werden muss →
model(). - Daten, die durch 3+ Ebenen gereicht werden („Prop Drilling“) → ein injizierter Service/Store.
- Kommunikation zwischen Geschwisterkomponenten ohne sinnvollen gemeinsamen Parent → ein Service.
- Globale Konfiguration (Theme, Sprache) →
InjectionToken/ Service.
Häufige Stolperfallen
this.value()im Konstruktor lesen: Die Inputs sind noch nicht gesetzt (Pflicht-Inputs werfen einen Fehler). Nutze sie incomputed,effectoder im Template.- Vom Child mutierte Objekte: Wenn du ein Objekt über einen Input erhältst und es veränderst, änderst du unbemerkt den Zustand des Parents. Emittiere stattdessen ein Event.
- Outputs, die wie DOM-Events heißen (
click,change), sorgen für Verwirrung — das native Event bubblet und löst den Handler zweimal aus. Wähle spezifische Namen.
Spickzettel
input<T>() // InputSignal<T | undefined>
input(default) // InputSignal<T>
input.required<T>() // Pflicht, Compile-Fehler, falls er fehlt
input(x, { transform, alias }) // booleanAttribute, numberAttribute, eigene Funktion
output<T>() // OutputEmitterRef<T> → .emit(value)
output<T>({ alias })
// Im Template des Parents
<app-x [value]="sig()" flag (changed)="onChanged($event)" />