Un composant parle à son parent via des inputs (les données qui entrent) et des outputs (les événements qui sortent). La règle classique tient toujours : données vers le bas, événements vers le haut. Ce qui a changé, c’est l’API : input() et output() sont des fonctions, et les inputs sont des Signals.
Un exemple complet
import { Component, booleanAttribute, computed, input, numberAttribute, output } from '@angular/core';
@Component({
selector: 'app-star-rating',
template: `
@for (star of stars(); track star) {
<button [class.on]="star <= value()" [disabled]="readonly()" (click)="rate.emit(star)">★</button>
}
`,
})
export class StarRating {
readonly value = input.required<number>(); // obligatoire
readonly max = input(5, { transform: numberAttribute }); // max="10" → 10
readonly readonly = input(false, { transform: booleanAttribute }); // <app-star-rating readonly />
readonly rate = output<number>();
// les inputs sont des Signals → dérivez directement avec computed, sans ngOnChanges
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}- Donnez une note : le parent reçoit l’événement (rate).
Options : required, transform, alias
| Forme | Type | Quand |
|---|---|---|
input(5) | InputSignal<number> | A une valeur par défaut raisonnable. |
input<string>() | InputSignal<string | undefined> | Vraiment optionnel. |
input.required<User>() | InputSignal<User> | Le composant n’a aucun sens sans lui. |
transform: booleanAttribute | accepte '', 'true' | Drapeaux : <app-x disabled />. |
transform: numberAttribute | accepte '10' | Nombres écrits comme attributs statiques. |
// Alias (le nom dans le template diffère de celui dans la classe) — rarement utilisé
readonly label = input('', { alias: 'ariaLabel' });
// Transform personnalisé : normalise la valeur à l’entrée
readonly tags = input([], {
transform: (value: string | string[]) => (Array.isArray(value) ? value : value.split(',')),
});
// Input optionnel sans valeur par défaut → le type inclut undefined
readonly hint = input<string>(); // InputSignal<string | undefined>
// Output avec alias
readonly closed = output<void>({ alias: 'close' });
this.closed.emit();Avant et maintenant
// ✓ L’API moderne : Signals + fonctions
export class StarRating {
readonly value = input.required<number>();
readonly max = input(5, { transform: numberAttribute });
readonly rate = output<number>();
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}// ✗ L’ancienne API, basée sur les décorateurs
export class StarRating implements OnChanges {
@Input({ required: true }) value!: number;
@Input({ transform: numberAttribute }) max = 5;
@Output() rate = new EventEmitter<number>();
stars: number[] = [];
ngOnChanges(changes: SimpleChanges) {
if (changes['max']) {
this.stars = Array.from({ length: this.max }, (_, i) => i + 1);
}
}
}Migration automatique : ng generate @angular/core:signal-input-migration et ng generate @angular/core:outputs.
Les inputs sont en lecture seule
export class Pager {
readonly page = input(1);
next() {
// ✗ les inputs sont en lecture seule dans le composant
// this.page.set(this.page() + 1);
// ✓ options :
// - émettre un événement et laisser le parent décider : this.pageChange.emit(this.page() + 1)
// - utiliser model() s’il s’agit d’une valeur bidirectionnelle → voir la page model()
// - utiliser linkedSignal(() => this.page()) pour une copie locale
}
}S’abonner aux outputs par programmation
// Vous pouvez vous abonner par programmation à un output (p. ex. sur un composant créé dynamiquement)
const ref = viewContainer.createComponent(StarRating);
const sub = ref.instance.rate.subscribe((value) => console.log(value));
// sub.unsubscribe(); — se fait automatiquement quand le composant est détruit
// Ou plus simplement, avec des bindings à la création (voir « Composants dynamiques ») :
viewContainer.createComponent(StarRating, {
bindings: [inputBinding('value', this.rating), outputBinding('rate', (v) => this.save(v))],
});- Le compilateur lève une erreur si vous liez deux fois le même input/output sur un même élément.
- Les attributs
data-*ne lient plus d’inputs ni d’outputs (p. ex.data-valuen’atteint plus l’inputvalue). - v22.2 : l’option
strictUnclaimedEventNamesdétecte les outputs mal orthographiés ((rated)au lieu de(rate)).
Trois niveaux plus bas : le prop drilling
Les inputs et les outputs relient un parent à son enfant direct. Quand les données vivent en haut et qu’on en a besoin trois niveaux plus bas, chaque composant intermédiaire doit déclarer un input pour les transmettre et un output pour faire remonter les événements — alors qu’aucun d’eux ne s’intéresse aux données.
signal: countinput: countinput: countoutput: addedoutput: addedoutput: addedoutput: added- Inputs déclarés
- 2
- Outputs déclarés
- 4
- Composants qui connaissent le panier
- 7
- Appuyez sur Ajouter au panier.
Avec des inputs et des outputs, ProductCard, ProductGrid et ShopPage n’existent que pour relayer l’événement, et chacun est un endroit de plus qui peut casser quand l’événement change de forme. Avec un store, seuls les deux composants qui utilisent vraiment le panier le connaissent. Gardez les inputs et les outputs pour la relation directe — ils rendent un composant réutilisable et son contrat visible — et passez à un service quand les données doivent traverser des niveaux ou atteindre un composant frère.
// chaque niveau intermédiaire déclare un output seulement pour relayer le clic
@Component({
selector: 'app-product-grid',
template: `<app-product-card (added)="added.emit()" />`,
})
export class ProductGrid {
readonly added = output();
}
// ProductCard, ShopPage : les mêmes trois lignes, une fois de plus pour chacun
// …et le compteur descend par l’autre branche, à travers d’autres inputs
@Component({
template: `
<app-header [count]="count()" />
<app-shop-page (added)="count.update((n) => n + 1)" />
`,
})
export class Shop {
protected readonly count = signal(0);
}Quand OUI, quand NON
- Communication directe parent ↔ enfant.
- Composants « dumb » / UI réutilisable (boutons, cartes, tableaux).
- Événements porteurs de sens :
(save),(delete),(select).
- Une valeur à synchroniser dans les deux sens →
model(). - Données transmises sur 3 niveaux ou plus (« prop drilling ») → un service/store injecté.
- Communication entre composants frères sans parent commun utile → un service.
- Configuration globale (thème, langue) →
InjectionToken/ service.
Pièges fréquents
- Lire
this.value()dans le constructeur : les inputs ne sont pas encore définis (les inputs obligatoires lèvent une erreur). Utilisez-les danscomputed,effectou le template. - Objets mutés par l’enfant : si vous recevez un objet via un input et que vous le modifiez, vous changez en silence l’état du parent. Émettez plutôt un événement.
- Outputs nommés comme des événements DOM (
click,change) prêtent à confusion : l’événement natif remonte et déclenche le handler deux fois. Choisissez des noms précis.
Aide-mémoire
input<T>() // InputSignal<T | undefined>
input(default) // InputSignal<T>
input.required<T>() // obligatoire, erreur de compilation s’il manque
input(x, { transform, alias }) // booleanAttribute, numberAttribute, votre propre fonction
output<T>() // OutputEmitterRef<T> → .emit(value)
output<T>({ alias })
// Dans le template du parent
<app-x [value]="sig()" flag (changed)="onChanged($event)" />