input(), output() et model() entre composants

À retenirinput() reçoit des données du parent sous forme de signal, output() émet des événements vers le parent, et model() crée une liaison bidirectionnelle [(valeur)]. La communication entre composants devient entièrement réactive.

Jusqu'ici, nos signals vivaient dans un seul composant. Voyons maintenant comment faire communiquer un parent et un enfant avec les API à base de signals, qui remplacent avantageusement les décorateurs @Input() et @Output() historiques.

input() : recevoir des données en signal

La fonction input() déclare une entrée du composant. Contrairement à @Input(), elle renvoie un signal en lecture seule : on lit la valeur reçue du parent en l'appelant.

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>(); // entrée OBLIGATOIRE
  readonly niveau = input(1);                   // entrée avec valeur par défaut
}

Côté parent :

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

Deux variantes utiles :

  • input.required<T>() : l'entrée est obligatoire. Si le parent l'oublie, Angular le signale à la compilation (avec strictTemplates).
  • Transformation : input(0, { transform: ... }) pour convertir la valeur reçue. Exemple courant, accepter disabled sans valeur :
import { booleanAttribute, numberAttribute } from '@angular/core';

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

Comme input() renvoie un signal, on peut le dériver directement avec computed() :

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

output() : émettre des événements vers le parent

output() déclare un événement de sortie typé. On le déclenche avec .emit(valeur) :

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

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

Côté parent, on écoute comme un événement classique :

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

output() est plus simple qu'un EventEmitter : pas d'héritage RxJS, juste emit(). Voici l'exemple parent-enfant complet, exécutable :

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

@Component({
  selector: 'app-note',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <article>
      <h3>{{ titre() }}</h3>
      <p>Note : {{ valeur() }} / 5</p>
      <button (click)="noter.emit(valeur() + 1)" [disabled]="valeur() >= 5">
        Augmenter
      </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>Avis produit</h1>
    <app-note titre="Qualité" [valeur]="qualite()" (noter)="qualite.set($event)" />
    <p>Note enregistrée : {{ qualite() }} / 5</p>
  `,
})
export class AppComponent {
  readonly qualite = signal(3);
}

Essayez en direct

input-output-note

model() : la liaison bidirectionnelle

model() combine une entrée et une sortie en un seul signal modifiable des deux côtés. C'est l'équivalent moderne du couple @Input() + @Output() qui rendait possible le [(banana-in-a-box)].

À l'intérieur du composant, un model est un signal writable : on le lit et on lui applique set/update. Côté parent, on le lie avec la syntaxe [(...)] :

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('')">Effacer</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>Formulaire</h1>
    <app-champ libelle="Pseudo" [(valeur)]="pseudo" />
    <p>Bonjour {{ pseudo() || 'inconnu' }} 👋</p>
  `,
})
export class AppComponent {
  readonly pseudo = signal('');
}

Quand l'utilisateur tape dans le champ, valeur change dans l'enfant, ce qui propage la nouvelle valeur au pseudo du parent — et inversement. Un seul signal, deux sens.

Essayez en direct

model-champ

input/output/model : lequel choisir ?

Besoin API Liaison parent
Recevoir une donnée du parent input() [valeur]
Notifier le parent d'un événement output() (evenement)
Donnée modifiable des deux côtés model() [(valeur)]

Règle simple : commencez par input() + output(). Ne passez à model() que lorsqu'une même valeur doit réellement être pilotée par le parent ET modifiée par l'enfant (champs de formulaire réutilisables, composants de type toggle, slider, date-picker).

En résumé

  • input() / input.required() : entrées en signal, dérivables avec computed, transformables via transform.
  • output() : événements typés émis avec .emit(), sans RxJS.
  • model() : liaison bidirectionnelle [(...)], un signal writable partagé parent-enfant.

Au chapitre 7, on connecte les signals au monde asynchrone avec l'interop RxJS : toSignal(), toObservable() et le pattern resource().

Nous utilisons Microsoft Clarity pour comprendre comment le site est utilisé et l'améliorer. En poursuivant votre navigation, vous l'acceptez. Vous pouvez le désactiver à tout moment.