← Back to all tutorials

Custom Event Binding (& @Output)

Send data from child to parent with @Output and EventEmitter — custom events for component communication.

Custom Event Binding (& @Output)

@Output() lets a child component emit events that the parent can listen to. This is how data flows upward — from child to parent.

Creating a Custom Event

// child: todo-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'app-todo-item',
    template: `
        <div class="todo-item">
            <span>{{ title }}</span>
            <button (click)="onDelete()">Delete</button>
        </div>
    `
})
export class TodoItemComponent {
    @Input() title: string = '';
    @Output() delete = new EventEmitter<string>();

    onDelete(): void {
        this.delete.emit(this.title);  // Emit the event with data
    }
}

Listening in the Parent

// parent: app.component.ts
export class AppComponent {
    todos = ['Learn Angular', 'Build an app', 'Deploy'];

    onTodoDeleted(title: string): void {
        this.todos = this.todos.filter(t => t !== title);
        console.log(`Deleted: ${title}`);
    }
}

// parent template
<app-todo-item 
    *ngFor="let todo of todos"
    [title]="todo"
    (delete)="onTodoDeleted($event)">
</app-todo-item>

The parent uses (delete)="handler($event)" — the same event binding syntax as native DOM events. $event contains the data passed to emit().

Emitting Complex Data

// child
interface TodoEvent {
    id: number;
    action: string;
}

@Output() todoAction = new EventEmitter<TodoEvent>();

onComplete(): void {
    this.todoAction.emit({ id: this.id, action: 'complete' });
}

onEdit(): void {
    this.todoAction.emit({ id: this.id, action: 'edit' });
}

// parent template
<app-todo-item 
    (todoAction)="handleAction($event)">
</app-todo-item>

// parent
handleAction(event: TodoEvent): void {
    if (event.action === 'complete') {
        this.markComplete(event.id);
    } else if (event.action === 'edit') {
        this.openEditor(event.id);
    }
}

@Input + @Output Together

// A reusable counter component:
@Component({
    selector: 'app-counter',
    template: `
        <div class="counter">
            <button (click)="decrement()">−</button>
            <span>{{ count }}</span>
            <button (click)="increment()">+</button>
        </div>
    `
})
export class CounterComponent {
    @Input() count: number = 0;
    @Output() countChange = new EventEmitter<number>();

    increment(): void {
        this.count++;
        this.countChange.emit(this.count);
    }

    decrement(): void {
        this.count--;
        this.countChange.emit(this.count);
    }
}

// Parent — two-way binding shorthand:
<app-counter [(count)]="itemCount"></app-counter>
<p>Items: {{ itemCount }}</p>

When @Output is named propertyChange (matching the @Input name + "Change"), you can use [()] two-way binding syntax!

Complete Data Flow

Parent Component
    │                          ▲
    │ @Input [data]            │ @Output (event)
    ▼                          │
Child Component  ─── emit() ───┘

Key Takeaways

  • @Output() + EventEmitter lets children send data to parents
  • Parent listens with (eventName)="handler($event)"
  • emit(data) sends data — $event receives it in the parent
  • Name an Output propChange to enable [(prop)] two-way shorthand
  • @Input = data flows down, @Output = events flow up