Inputs carry data from a parent to a child. To go the other way, a child announces that something happened and the parent decides what to do. Angular calls these announcements outputs.
What you will learn
- How to declare an output with
output()and send values withemit() - How a parent listens to an output
- How
model()combines an input and an output for two-way binding
Declare and emit an output
// src/app/enroll-card/enroll-card.ts
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-enroll-card',
template: `
<article class="card">
<h3>{{ title() }}</h3>
<button type="button" (click)="enroll()">Enroll</button>
</article>
`,
styles: `
.card { border: 1px solid #cbd5e1; border-radius: 8px; padding: 12px; margin: 8px 0; max-width: 320px; }
`,
})
export class EnrollCard {
readonly id = input.required<number>();
readonly title = input.required<string>();
// announces "the user wants to enroll", carrying the course id
readonly enrolled = output<number>();
protected enroll(): void {
this.enrolled.emit(this.id());
}
}
output<number>() creates an event source that carries a number. Calling emit(value) sends the value to whoever is listening. The child does not know who that is, which keeps it reusable.
Listen in the parent
The parent uses parentheses, just like for a click, with the name of the output. $event holds the value that was emitted:
// src/app/app.ts
import { Component, signal } from '@angular/core';
import { EnrollCard } from './enroll-card/enroll-card';
import { Rating } from './rating/rating';
@Component({
imports: [EnrollCard, Rating],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected readonly enrolledIds = signal<number[]>([]);
protected readonly stars = signal(3);
protected onEnrolled(id: number): void {
this.enrolledIds.update((ids) => (ids.includes(id) ? ids : [...ids, id]));
}
}
<!-- src/app/app.html -->
<main>
<h1>Outputs</h1>
<app-enroll-card [id]="1" title="Angular for beginners" (enrolled)="onEnrolled($event)" />
<app-enroll-card [id]="2" title="TypeScript essentials" (enrolled)="onEnrolled($event)" />
<p>Enrolled in {{ enrolledIds().length }} course(s).</p>
<h2>Rate this course</h2>
<app-rating [(value)]="stars" />
<p>Your rating: {{ stars() }} / 5</p>
</main>
When you press Enroll, the child emits its id, the parent's onEnrolled runs, and the parent updates its own signal. The data flows down through inputs and the events flow up through outputs.
Two-way binding with model()
Sometimes a child needs to both show a value and change it, such as a rating widget. A model() is an input that can also be written to. The parent binds with the "banana in a box" syntax, [(value)], which is a property binding and an event binding in one:
// src/app/rating/rating.ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-rating',
template: `
@for (n of [1, 2, 3, 4, 5]; track n) {
<button type="button" (click)="value.set(n)" [attr.aria-pressed]="n <= value()" [attr.aria-label]="n + ' stars'">
{{ n <= value() ? '★' : '☆' }}
</button>
}
`,
})
export class Rating {
readonly value = model(0);
}
When the child calls value.set(n), the parent's stars signal changes too, and the other way round. Use two-way binding for small, focused widgets. For everything else, plain inputs and outputs are easier to reason about.
Naming outputs
Name outputs after what happened, not after what the parent should do: enrolled, selected, closed. Avoid names of native events such as click, which can cause confusion.
Try it yourself
- Add a "Remove" button to
EnrollCardwith a second output calledremoved. - Make the parent hide the enrolled message when the list is empty.
- Change the star widget to also show the word "Great!" when the value is 5.
Quick check
- What does
output<number>()create? - How does a parent receive the value that was emitted?
- What is the difference between
input()andmodel()?
Next, learn to format data in templates with Angular pipes. See also the official outputs guide.