Wenn Daten von der Komponente ins Template über Property-Binding fließen, ist der Rückweg das Event-Binding. Mit runden Klammern lauschst du auf DOM-Events oder auf Custom-Events einer Child-Komponente; mit der Kombination aus beiden Welten — der [(banana)]-Syntax — entsteht echtes Two-Way-Binding.
Stand ist Angular 21 (stable, Nov 2025). Mit der model()-Funktion (seit v17.2, GA seit v19) hat Angular einen modernen, signal-basierten Weg geschaffen, Two-Way-Bindings zwischen Komponenten zu deklarieren — ohne ein separates Input/Output-Paar von Hand zu pflegen.
Event-Binding mit (event)
Ein Event-Binding ist ein Statement, das bei Auftreten eines DOM- oder Component-Events ausgeführt wird. Die Syntax ist kompakt: (eventName)="statement". Anders als Template-Expressions dürfen Event-Statements Zuweisungen enthalten — gleichzeitig sind Pipes verboten. Diese Asymmetrie ist Designentscheidung: Events sollen Seiteneffekte produzieren dürfen, ohne dass Pipes als Trick-Operator missbraucht werden.
Im Statement steht die spezielle Variable $event zur Verfügung. Bei nativen DOM-Events ist sie das Event-Objekt (MouseEvent, KeyboardEvent, InputEvent, …); bei Custom-Outputs einer Child-Komponente der Wert, der in .emit() übergeben wurde.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-contact-form',
standalone: true,
template: `
<form (submit)="onSubmit($event)">
<input
type="text"
[value]="name()"
(input)="onNameInput($event)"
placeholder="Name"
/>
<textarea
(focus)="trackFocus('message')"
(blur)="trackBlur('message')"
(input)="message.set(($event.target as HTMLTextAreaElement).value)"
>{{ message() }}</textarea>
<button type="submit" [disabled]="!canSubmit()">Senden</button>
</form>
<p>Aktueller Name: {{ name() }}</p>
<p>Status: {{ status() }}</p>
`,
})
export class ContactFormComponent {
name = signal('');
message = signal('');
status = signal('idle');
onNameInput(event: Event): void {
const input = event.target as HTMLInputElement;
this.name.set(input.value);
}
onSubmit(event: SubmitEvent): void {
event.preventDefault();
this.status.set('submitting');
// ... API-Call
}
trackFocus(field: string): void {
this.status.set(`focus:${field}`);
}
trackBlur(field: string): void {
this.status.set(`blur:${field}`);
}
canSubmit(): boolean {
return this.name().length > 0 && this.message().length > 0;
}
}Eigene Events einer Child-Komponente abfangen
Custom-Events einer Child-Komponente werden mit derselben (eventName)-Syntax abonniert. Aus Sicht des Parents ist nicht sichtbar, ob es sich um ein natives DOM-Event oder ein Component-Output handelt — das ist Teil von Angulars konsistenter Template-Syntax.
Die moderne Definition auf Child-Seite erfolgt mit output<T>() (siehe der ausführliche Artikel zum Thema Data Output); die klassische mit @Output() name = new EventEmitter<T>(). Beide produzieren in der Parent-Sicht identische Bindings.
import { Component, output, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="increment()">+</button>
<span>{{ count() }}</span>
<button (click)="decrement()">−</button>
`,
})
export class CounterComponent {
count = signal(0);
// Custom-Event mit Payload (number)
countChanged = output<number>();
increment(): void {
this.count.update(c => c + 1);
this.countChanged.emit(this.count());
}
decrement(): void {
this.count.update(c => c - 1);
this.countChanged.emit(this.count());
}
}<!-- $event ist hier vom Typ number (der Payload des Outputs) -->
<app-counter (countChanged)="onCountChanged($event)" />
<!-- Mehrere Events gleichzeitig: jedes mit eigenem Handler -->
<app-counter
(countChanged)="logChange($event)"
(mouseenter)="hover.set(true)"
(mouseleave)="hover.set(false)"
/>DOM-Events typisieren
$event ist im Template per Default vom Typ Event — die generische Browser-Schnittstelle. Für die meisten Anwendungsfälle ist das zu schwach: man möchte auf target.value (Input), key (Keyboard) oder clientX/Y (Mouse) zugreifen. Es gibt zwei verbreitete Wege, hier zu schärferen Typen zu kommen.
Variante A — Cast im Handler: Die Methode in der Klasse nimmt Event entgegen und castet selbst. Vorteil: Template bleibt knapp. Nachteil: der Cast wandert in die Klasse, was bei vielen Handlern repetitiv wird.
Variante B — Template-Reference-Variable: Die Referenz auf das Element wird mit #name gesetzt; im Event-Handler wird dann direkt die getypte Property gelesen. Vorteil: kein Cast, keine $event-Indirektion. Nachteil: Funktioniert nur für direkt referenzierbare Properties.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-typed-events',
standalone: true,
template: `
<!-- Variante A: Cast im Handler -->
<input (input)="onInputA($event)" />
<!-- Variante B: Template-Reference-Variable -->
<input #search (input)="onInputB(search.value)" />
<!-- Inline-Cast: kompakt, manchmal lesbarer als ein Method-Call -->
<input
(input)="value.set(($event.target as HTMLInputElement).value)"
/>
<!-- Keyboard-Event mit konkretem Typ -->
<input (keydown)="onKey($event)" />
<p>Wert: {{ value() }}</p>
`,
})
export class TypedEventsComponent {
value = signal('');
onInputA(event: Event): void {
const target = event.target as HTMLInputElement;
this.value.set(target.value);
}
onInputB(value: string): void {
this.value.set(value);
}
onKey(event: KeyboardEvent): void {
if (event.key === 'Enter') {
console.log('Enter gedrückt');
}
}
}Key-Modifier und Mouse-Modifier
Statt im Handler explizit event.key === 'Enter' zu prüfen, lässt sich die Bedingung direkt in den Event-Namen kodieren. Angular bietet dafür ein Modifier-System, das mit Punkt-Notation arbeitet: (keyup.enter) feuert nur bei der Enter-Taste, (keyup.shift.enter) nur bei Shift+Enter.
Die Kombination ist beliebig: alt, control, meta, shift lassen sich mischen, anschließend folgt der Key-Name (oder das Code-Suffix .code für OS-unabhängiges Handling — dort heißt die Taste z. B. KeyA statt a).
| Modifier-Syntax | Auslöser |
|---|---|
(keyup.enter) | Loslassen der Enter-Taste |
(keydown.escape) | Drücken von Escape |
(keyup.space) | Loslassen der Leertaste |
(keydown.tab) | Drücken von Tab |
(keyup.arrowup) | Pfeil hoch loslassen |
(keydown.shift.enter) | Shift gedrückt halten + Enter |
(keydown.control.s) | Strg/Cmd + S (auf Mac: control = ctrl, nicht cmd) |
(keydown.meta.k) | Cmd+K (Mac) / Win+K (Windows) |
(keydown.alt.shift.f) | Alt + Shift + F |
(keyup.code.KeyA) | Loslassen der physischen Taste „A” (Layout-unabh.) |
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-search-box',
standalone: true,
template: `
<div class="search">
<input
#input
type="search"
[value]="query()"
(input)="query.set(input.value)"
(keyup.enter)="onSubmit()"
(keyup.escape)="onClear(input)"
(keydown.control.k)="$event.preventDefault(); input.focus()"
placeholder="Suchen… (Strg+K zum Fokussieren, Esc zum Leeren)"
/>
@if (lastSubmitted()) {
<p>Letzte Suche: {{ lastSubmitted() }}</p>
}
</div>
`,
})
export class SearchBoxComponent {
query = signal('');
lastSubmitted = signal<string | null>(null);
onSubmit(): void {
if (this.query().length === 0) return;
this.lastSubmitted.set(this.query());
}
onClear(input: HTMLInputElement): void {
this.query.set('');
input.value = '';
}
}passive und globale Events
Bei sehr häufigen Events (scroll, wheel, touchmove) bietet der Browser den passive: true-Modus an: das Event-Listener-Callback verspricht, kein preventDefault() aufzurufen. Der Browser kann dann das Event sofort an das Compositor-Thread weiterreichen, ohne auf JavaScript zu warten — entscheidend für 60-fps-Scrolling.
In Angular gibt es dafür den globalen EVENT_MANAGER_PLUGINS-Mechanismus und für die häufigsten Fälle den eingebauten provideEventDispatch()-Provider sowie das HostListener-Decorator-Option. Direkt im Template ist die Modifier-Notation passive nicht als Punkt-Syntax verfügbar — der gängige Weg läuft über host-Listener oder eigene Direktiven.
Globale Events (auf window, document, body) werden im Template selten gebraucht; im Component-Code übernehmen das Host-Listener mit den Pseudo-Targets window:resize, document:keydown, body:click.
import { Component, HostListener, signal } from '@angular/core';
@Component({
selector: 'app-scroll-watcher',
standalone: true,
host: {
// Host-Listener: auf dem Component-Root-Element
'(scroll)': 'onScroll($event)',
},
template: `
<p>Scroll-Position: {{ scrollY() }}</p>
`,
})
export class ScrollWatcherComponent {
scrollY = signal(0);
onScroll(event: Event): void {
const target = event.target as HTMLElement;
this.scrollY.set(target.scrollTop);
}
// Globaler Listener auf window
@HostListener('window:resize', ['$event'])
onResize(event: UIEvent): void {
console.log('Resize:', (event.target as Window).innerWidth);
}
// Globaler Listener auf document
@HostListener('document:keydown.escape')
onGlobalEscape(): void {
console.log('Escape gedrückt — globaler Handler');
}
}Two-Way-Binding mit [(prop)] (Banana-in-a-Box)
Two-Way-Binding ist syntaktisch nichts Magisches: [(name)]="value" ist exakt äquivalent zu [name]="value" (nameChange)="value = $event". Die Klammer-um-Klammer-Schreibweise (Banana-in-a-Box) macht visuell sichtbar, dass beide Richtungen aktiv sind. Voraussetzung: Das Ziel deklariert ein Input name und ein passendes Output nameChange (exakter Name, exakte Schreibweise).
Mit klassischen Inputs/Outputs muss diese Konvention von Hand gepflegt werden. Mit model() (siehe nächster Abschnitt) übernimmt Angular die Verkabelung automatisch.
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-toggle-legacy',
standalone: true,
template: `
<button (click)="flip()" [class.on]="value">
{{ value ? 'AN' : 'AUS' }}
</button>
`,
})
export class LegacyToggleComponent {
// Pflicht: Input + gleichnamiger Output mit Suffix "Change"
@Input() value = false;
@Output() valueChange = new EventEmitter<boolean>();
flip(): void {
this.value = !this.value;
this.valueChange.emit(this.value);
}
}<!-- Banana-in-a-Box: setzt value UND empfängt Updates -->
<app-toggle-legacy [(value)]="enabled" />
<!-- Äquivalent ausgeschrieben -->
<app-toggle-legacy
[value]="enabled"
(valueChange)="enabled = $event"
/>
<p>Aktueller Zustand: {{ enabled }}</p>Two-Way mit model() ab v17.2
Die model()-Funktion ist das signal-basierte Pendant zur klassischen Input/Output-Verkabelung. Sie erzeugt ein ModelSignal, das gleichzeitig lesbar ist (wie ein Signal: value()) und schreibbar (wie ein Writable Signal: value.set(x)). Beim Schreiben emittiert Angular automatisch das passende Change-Event nach außen — die Banana-in-a-Box-Konvention wird also vom Framework gepflegt.
Verfügbar als drei Overloads: model<T>() ohne Default (T | undefined), model<T>(initial) mit Default und model.required<T>() für Bindings, die der Aufrufer zwingend setzen muss.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `
<button (click)="flip()" [class.on]="value()">
{{ value() ? 'AN' : 'AUS' }}
</button>
`,
})
export class ToggleComponent {
// Ein einziger Aufruf — Angular erzeugt Input + Output automatisch
value = model<boolean>(false);
flip(): void {
// Schreiben emittiert valueChange automatisch
this.value.update(v => !v);
}
}import { Component, signal } from '@angular/core';
import { ToggleComponent } from './modern-toggle.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [ToggleComponent],
template: `
<!-- Two-Way auf ein Signal (sehr typisch) -->
<app-toggle [(value)]="enabled" />
<!-- Two-Way auf eine normale Property funktioniert auch -->
<app-toggle [(value)]="plainBool" />
<p>Aus Sicht des Parents: {{ enabled() }}</p>
`,
})
export class ParentComponent {
enabled = signal(false);
plainBool = false;
}| Aspekt | Klassisch (Input + Output) | Modern (model()) |
|---|---|---|
| Anzahl Deklarationen | 2 Felder + manueller Emit | 1 Feld, automatischer Emit |
| Naming-Konvention | Manuell — Tippfehler möglich | Automatisch — Compiler-erzwungen |
| Lesen im Template | {{ value }} | {{ value() }} |
| Schreiben in Klasse | this.value = x; this.valueChange.emit(x) | this.value.set(x) |
| Required-Variante | @Input({ required: true }) + Output | model.required<T>() |
| Reaktivität | Property + Change-Detection | Signal — fein granular |
ngModel im Detail
ngModel ist die Two-Way-Direktive für Form-Controls aus dem FormsModule (Template-Driven Forms). Hinter den Kulissen ist es ein normales Two-Way-Binding: das Input heißt ngModel, das Output ngModelChange. Plus eine Bonusfunktion: bei einem name-Attribut registriert sich das Control automatisch in der umgebenden <form> und wird Teil des NgForm-Validation-Status.
Damit ngModel funktioniert, muss FormsModule in der Standalone-Komponente importiert werden — bei NgModule-Apps reicht der Import einmal im Feature-Modul. Ohne diesen Import scheitert das Template mit „Can’t bind to ‘ngModel’ since it isn’t a known property”.
import { Component, signal } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
@Component({
selector: 'app-login-form',
standalone: true,
imports: [FormsModule],
template: `
<form #form="ngForm" (submit)="onSubmit(form)">
<label>
E-Mail
<input
type="email"
name="email"
[(ngModel)]="email"
required
email
#emailField="ngModel"
/>
</label>
@if (emailField.invalid && emailField.touched) {
<small class="error">Bitte gültige E-Mail eingeben.</small>
}
<label>
Passwort
<input
type="password"
name="password"
[(ngModel)]="password"
required
minlength="8"
/>
</label>
<button type="submit" [disabled]="form.invalid">Anmelden</button>
</form>
<pre>{{ debug() }}</pre>
`,
})
export class LoginFormComponent {
email = '';
password = '';
debug = signal('');
onSubmit(form: NgForm): void {
if (form.invalid) return;
this.debug.set(JSON.stringify(form.value, null, 2));
}
}Für komplexe Forms (dynamische Validierung, programmatische Wertänderung, asynchrone Validators) bietet Angular die Reactive Forms (FormControl, FormGroup) als Alternative — sie sind konzeptuell näher an einer expliziten State-Machine und lassen sich besser testen. Der ausführliche Vergleich folgt im Forms-Kapitel.
Häufige Fragen
Was ist eigentlich „$event“?
Eine spezielle Variable, die nur in Event-Handler-Statements existiert. Bei nativen DOM-Events ist sie das jeweilige Event-Objekt (MouseEvent, KeyboardEvent, …), bei Custom-Outputs einer Child-Komponente der Wert, der via .emit(value) übergeben wurde. In Templates außerhalb von Event-Handlern (also in Property-Bindings oder Interpolation) ist $event nicht verfügbar.
Wie verhindere ich Event-Bubbling?
Direkt im Handler mit $event.stopPropagation(). Eine Modifier-Syntax wie (click.stop) existiert in Angular nicht — anders als z. B. in Vue. Bei preventDefault() reicht es, im Handler explizit aufzurufen oder das Statement mit ; return false abzuschließen (eher unüblich).
Warum bricht [(ngModel)] mit „Can't bind to ngModel“?
Fast immer fehlt der Import von FormsModule. In Standalone-Komponenten gehört es in das imports-Array; in NgModule-Apps in das jeweilige Feature-Modul. Der Compiler kennt die Direktive sonst nicht und meldet, das Property gebe es nicht.
Kann ich auf Custom-Events „passive: true“ setzen?
Nein. Der passive-Hint ist eine Browser-API für native DOM-Events; Custom-Outputs einer Component sind keine DOM-Events und kennen den Modus nicht. Für Performance-kritische Streams aus einem Output ist eher debounceTime oder ein computed()-basiertes Throttling der richtige Hebel.
Wann model() vs. input/output-Paar?
model() für tatsächliches Two-Way — der Wert fließt symmetrisch in beide Richtungen, der Component-Name beschreibt den Zustand (z. B. value, open, selected). Getrenntes input()+output(), wenn Eingabe und Ausgabe semantisch verschieden sind: items rein, itemDeleted raus.
Werden Event-Handler bei OnPush ausgeführt?
Ja — und das ist sogar einer der Hauptmechanismen, mit denen OnPush funktioniert. Ein Event-Listener auf einem Element oder Output innerhalb der Komponente markiert die Komponente als dirty, sodass die Change-Detection im nächsten Tick durch sie hindurchläuft. Ohne dieses Verhalten wäre OnPush praktisch unbrauchbar.
Wie debounce/throttle ich (input)?
Nicht im Template — Angular bietet keine eingebaute Modifier-Syntax dafür. Stattdessen im Component-Code: ein Subject in den Handler, dann mit RxJS debounceTime(300) verarbeiten und das Ergebnis in ein Signal kippen. Alternativ aus einem Signal heraus mit toObservable() und debounceTime weiterverarbeiten — beide Wege sind sauber, der RxJS-Weg ist der etabliertere.
Weiterführende Ressourcen
Externe Quellen
- Event Listeners – Angular.dev
- Two-Way Binding – Angular.dev
- model() API – Angular.dev
- FormsModule – Angular.dev