Angular Component Outputs and Two-Way Binding with model()

Angular Tutorial: Beginner to Advanced · 6. Reusable components · 4 min read · Updated

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 with emit()
  • 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

  1. Add a "Remove" button to EnrollCard with a second output called removed.
  2. Make the parent hide the enrolled message when the list is empty.
  3. Change the star widget to also show the word "Great!" when the value is 5.

Quick check

  1. What does output<number>() create?
  2. How does a parent receive the value that was emitted?
  3. What is the difference between input() and model()?

Next, learn to format data in templates with Angular pipes. See also the official outputs guide.

Frequently asked questions

Is output() the same as EventEmitter?

The older approach used @Output() with an EventEmitter. The output() function replaces it and is simpler. It is not built on RxJS, so you only call emit().

When should I use model() instead of input() and output()?

Use it when a child edits a value that the parent also owns, such as a rating or a toggle. If the parent needs to validate or change the value first, use an input and an output instead.