Während Inputs dafür da sind, Daten von oben nach unten (von der Parent- an die Child-Komponente) zu reichen, dienen Outputs dem exakt umgekehrten Weg. Mit Outputs kann eine Child-Komponente eigene, benutzerdefinierte Events auslösen (emitten), auf die eine Parent-Komponente lauschen kann.
Genau wie bei den Inputs gab es in Angular v17+ ein API-Update, das den Code sicherer und lesbarer macht und die enge Bindung an RxJS auflöst. Dieser Artikel beleuchtet die Details und Mechanismen hinter der modernen Event-Architektur.
Architektur: Wie Outputs funktionieren
In der Frontend-Welt ist das Prinzip des “Unidirectional Data Flow” weit verbreitet. Daten fließen von oben (Parent) nach unten (Child). Ein Child sollte diese Daten idealerweise niemals direkt manipulieren. Wenn ein Child (z.B. ein Lösch-Button) eine Aktion ausführt, sendet es lediglich eine Nachricht (Event) nach oben. Der Parent entscheidet dann, wie er auf diese Nachricht reagiert (z.B. indem er einen Datensatz löscht und die neuen Daten wieder nach unten reicht).
Dieses “Senden von Nachrichten” wird in Angular über Outputs abgebildet.
Die moderne output() Funktion
Analog zur Signal-basierten input() API bietet Angular die Funktion output(), um Events typsicher zu deklarieren. Sie ersetzt den alten @Output()-Dekorator und die EventEmitter-Klasse aus RxJS durch eine sehr schlanke, interne Angular-API.
Die Funktion output<T>() gibt ein Objekt vom Typ OutputEmitterRef<T> zurück. Dieses Objekt stellt ausschließlich die Methode .emit(value: T) bereit.
Einfaches Event (ohne Payload)
Wenn du dem Parent nur mitteilen möchtest, dass etwas passiert ist (z.B. ein Klick oder ein Hover), nutzt du den Typ void.
import { Component, output } from '@angular/core';
@Component({
selector: 'app-delete-button',
template: `
<button (click)="onDeleteClick()" class="btn btn-danger">
Löschen
</button>
`,
standalone: true
})
export class DeleteButtonComponent {
// Deklaration: Das Event heißt "itemDeleted" und transportiert keine Daten.
itemDeleted = output<void>();
onDeleteClick() {
// Die emit() Methode feuert das Event an den Parent ab.
this.itemDeleted.emit();
}
}Event mit Payload (Daten mitsenden)
Meistens musst du dem Parent zusätzlichen Kontext mitgeben. Ein Suchfeld muss den Such-String senden, ein Pagination-Modul die ausgewählte Seitenzahl. Hierfür definierst du den exakten Typ (z. B. number, string oder ein komplexes Interface).
import { Component, output } from '@angular/core';
export interface UserSaveEvent {
username: string;
age: number;
isValid: boolean;
}
@Component({
selector: 'app-user-form',
template: `
<button (click)="save()">Speichern</button>
`,
standalone: true
})
export class UserFormComponent {
// Das Event erzwingt nun streng, dass ein Objekt vom Typ UserSaveEvent mitgeschickt wird.
userSaved = output<UserSaveEvent>();
save() {
// Fehlt hier ein Feld aus dem Interface, meckert der TypeScript-Compiler sofort.
this.userSaved.emit({
username: 'MaxMustermann',
age: 28,
isValid: true
});
}
}Binding und Auslesen in der Parent-Komponente
Um das Event abzufangen, nutzt die Parent-Komponente im HTML die runden Klammern (eventName)="methode()". Die mitgesendeten Daten (der Payload) sind über die spezielle Template-Variable $event verfügbar.
<!-- Ein void-Event: Wir brauchen kein $event, sondern rufen einfach eine Methode auf -->
<app-delete-button (itemDeleted)="showToast('Item wurde gelöscht')" />
<!-- Ein Payload-Event: Wir reichen das $event (das exakt dem UserSaveEvent Interface entspricht) an den Controller weiter -->
<app-user-form (userSaved)="handleSave($event)" />Interoperabilität mit RxJS (Observables)
Oft ist deine Anwendungslogik stark asynchron und basiert auf RxJS Observables. Die Signal-basierten APIs von Angular bieten fantastische Hilfsfunktionen, um Observables fließend in Outputs zu verwandeln. Du findest diese im Paket @angular/core/rxjs-interop.
outputFromObservable()
Stell dir vor, du hast einen internen RxJS-Stream in deiner Komponente (z.B. eine Polling-Funktion oder ein gekapseltes WebSocket-Ereignis) und möchtest jeden emittierten Wert dieses Streams direkt als Angular-Output nach außen geben.
import { Component } from '@angular/core';
import { outputFromObservable } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-timer',
template: `<p>Timer läuft im Hintergrund...</p>`,
standalone: true
})
export class TimerComponent {
// Generiert jede Sekunde eine fortlaufende Zahl (0, 1, 2...)
private timer$ = interval(1000);
// Verwandelt das RxJS Observable automatisch in einen Angular-Output.
// Sobald die Parent-Komponente darauf lauscht, wird abonniert.
// Wird die Parent-Komponente zerstört, wird vollautomatisch entabonniert (kein Memory Leak!).
tick = outputFromObservable(this.timer$);
}outputToObservable()
Der umgekehrte Weg: Manchmal erwartet eine externe Bibliothek zwingend ein RxJS Observable, aber du hast nur einen modernen Angular output(). Mit outputToObservable zapfst du den Output an und machst aus den fließenden Events einen reaktiven RxJS-Stream.
Der klassische @Output() Dekorator
In Codebases vor Angular 17+ basieren sämtliche Events auf dem @Output()-Dekorator und der Klasse EventEmitter.
Der EventEmitter erbt unter der Haube von RxJS Subject. Das bedeutet, alte Outputs waren streng genommen RxJS-Streams. Das Angular-Team hat sich bewusst davon gelöst, um Angular (und sein neues zoneless Change-Detection System) langfristig von der schweren Abhängigkeit zu RxJS zu entkoppeln.
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-legacy-button',
template: `<button (click)="clickAction()">Speichern</button>`
})
export class LegacyButtonComponent {
// Klassische Definition mit EventEmitter
@Output() btnClicked = new EventEmitter<string>();
clickAction() {
this.btnClicked.emit('Hallo Welt');
}
}Häufige Stolperfallen und Best Practices
Angular Outputs bubbeln nicht
Im Gegensatz zu nativen DOM-Events (wie ein Klick auf ein div, das sich durch alle Eltern-Elemente nach oben durchreicht), “bubbeln” Angular Outputs niemals. Wenn eine Child-Komponente ein Event feuert, kann nur die direkte Parent-Komponente darauf lauschen. Willst du ein Event vom Grandchild zum Grandparent reichen, musst du es stur in jeder Schicht neu fangen und emitten (Prop-Drilling) oder besser: einen State-Service einsetzen.
Die 'on'-Präfix Falle
Vermeide es strikt, deine Outputs mit on beginnen zu lassen (z.B. onClick oder onSave). Das Template-Binding passiert ohnehin mit Klammern (click)=”…”. Ein Präfix zwingt den Verwender zu extrem seltsamem Code wie (onSave)=“doSomething()”. Nenne das Event stattdessen einfach save oder itemDeleted.
Zwei-Wege-Bindung Namenskonvention
Wenn du das klassische @Output nutzt, um eine Two-Way-Binding wie [(size)] aufzubauen, muss das Output zwingend den exakten Namen des Inputs plus das Wort Change tragen (also Input: size, Output: sizeChange). Mit der neuen Signal-Funktion model() übernimmt Angular diese strikte Namenslogik völlig automatisch für dich.