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.

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

TypeScript counter.component.ts
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());
    }
}
HTML parent.component.html
<!-- $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.

TypeScript typed-events.component.ts
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-SyntaxAuslö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.)
TypeScript search-box.component.ts
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.

TypeScript scroll-watcher.component.ts
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.

TypeScript legacy-toggle.component.ts
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);
    }
}
HTML parent.component.html
<!-- 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.

TypeScript modern-toggle.component.ts
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);
    }
}
TypeScript parent-with-model.component.ts
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;
}
AspektKlassisch (Input + Output)Modern (model())
Anzahl Deklarationen2 Felder + manueller Emit1 Feld, automatischer Emit
Naming-KonventionManuell — Tippfehler möglichAutomatisch — Compiler-erzwungen
Lesen im Template{{ value }}{{ value() }}
Schreiben in Klassethis.value = x; this.valueChange.emit(x)this.value.set(x)
Required-Variante@Input({ required: true }) + Outputmodel.required<T>()
ReaktivitätProperty + Change-DetectionSignal — 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”.

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

/ Weiter

Zurück zu Templates & Control Flow

Zur Übersicht