input(), output() and model() between components

Key takeawayinput() receives data from the parent as a signal, output() emits events to the parent, and model() creates a two-way binding [(valeur)]. Communication between components becomes fully reactive.

So far, our signals lived inside a single component. Let's now see how to make a parent and a child communicate with the signal-based APIs, which advantageously replace the historical @Input() and @Output() decorators.

input(): receiving data as a signal

The input() function declares a component input. Unlike @Input(), it returns a read-only signal: you read the value received from the parent by calling it.

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-badge',
  standalone: true,
  template: `<span>{{ libelle() }} ({{ niveau() }})</span>`,
})
export class BadgeComponent {
  readonly libelle = input.required<string>(); // REQUIRED input
  readonly niveau = input(1);                   // input with a default value
}

On the parent side:

<app-badge libelle="Expert" [niveau]="3" />

Two useful variants:

  • input.required<T>(): the input is required. If the parent forgets it, Angular reports it at compile time (with strictTemplates).
  • Transformation: input(0, { transform: ... }) to convert the received value. A common example, accepting disabled without a value:
import { booleanAttribute, numberAttribute } from '@angular/core';

readonly desactive = input(false, { transform: booleanAttribute }); // <cmp desactive>
readonly age = input(0, { transform: numberAttribute });            // age="42" → 42

Since input() returns a signal, you can derive it directly with computed():

readonly prix = input.required<number>();
readonly prixTTC = computed(() => this.prix() * 1.2);

output(): emitting events to the parent

output() declares a typed output event. You fire it with .emit(value):

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-note',
  standalone: true,
  template: `
    <p>Rating: {{ valeur() }} / 5</p>
    <button (click)="noter.emit(valeur() + 1)" [disabled]="valeur() >= 5">
      Increase
    </button>
  `,
})
export class NoteComponent {
  readonly valeur = input(0);
  readonly noter = output<number>(); // OutputEmitterRef<number>
}

On the parent side, you listen to it like a classic event:

<app-note [valeur]="qualite()" (noter)="qualite.set($event)" />

output() is simpler than an EventEmitter: no RxJS inheritance, just emit(). Here is the full, runnable parent-child example:

import { Component, input, output, signal, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-note',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <article>
      <h3>{{ titre() }}</h3>
      <p>Rating: {{ valeur() }} / 5</p>
      <button (click)="noter.emit(valeur() + 1)" [disabled]="valeur() >= 5">
        Increase
      </button>
    </article>
  `,
})
export class NoteComponent {
  readonly titre = input.required<string>();
  readonly valeur = input(0);
  readonly noter = output<number>();
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NoteComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h1>Product review</h1>
    <app-note titre="Qualité" [valeur]="qualite()" (noter)="qualite.set($event)" />
    <p>Saved rating: {{ qualite() }} / 5</p>
  `,
})
export class AppComponent {
  readonly qualite = signal(3);
}

Try it live

input-output-note

model(): two-way binding

model() combines an input and an output into a single signal modifiable from both sides. It is the modern equivalent of the @Input() + @Output() pair that made the [(banana-in-a-box)] possible.

Inside the component, a model is a writable signal: you read it and apply set/update to it. On the parent side, you bind it with the [(...)] syntax:

import { Component, input, model, signal, ChangeDetectionStrategy } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-champ',
  standalone: true,
  imports: [FormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <label>
      {{ libelle() }}:
      <input [(ngModel)]="valeur" />
    </label>
    <button (click)="valeur.set('')">Clear</button>
  `,
})
export class ChampComponent {
  readonly libelle = input('Champ');
  readonly valeur = model(''); // ModelSignal<string>
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChampComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h1>Form</h1>
    <app-champ libelle="Pseudo" [(valeur)]="pseudo" />
    <p>Hello {{ pseudo() || 'inconnu' }} 👋</p>
  `,
})
export class AppComponent {
  readonly pseudo = signal('');
}

When the user types in the field, valeur changes in the child, which propagates the new value to the parent's pseudo — and vice versa. One signal, both directions.

Try it live

model-champ

input/output/model: which one to choose?

Need API Parent binding
Receive data from the parent input() [valeur]
Notify the parent of an event output() (evenement)
Data modifiable from both sides model() [(valeur)]

Simple rule: start with input() + output(). Only move to model() when one same value truly needs to be driven by the parent AND modified by the child (reusable form fields, toggle, slider, date-picker style components).

Key points

  • input() / input.required(): inputs as signals, derivable with computed, transformable via transform.
  • output(): typed events emitted with .emit(), without RxJS.
  • model(): two-way binding [(...)], a writable signal shared between parent and child.

In chapter 7, we connect signals to the asynchronous world with RxJS interop: toSignal(), toObservable() and the resource() pattern.

We use Microsoft Clarity to understand how the site is used and improve it. By continuing to browse, you accept it. You can disable it at any time.