Dans Angular, faire remonter une information d’un composant enfant vers son parent est une opération banale en apparence, mais elle conditionne souvent la clarté de toute l’architecture frontend. Le duo output() et EventEmitter sert précisément à ça, avec une version moderne de l’API qui mérite d’être comprise avant d’être appliquée partout. Ici, je vais montrer comment cela fonctionne, quand l’utiliser, quand préférer une autre approche, et quels pièges évitent de transformer un simple événement en code difficile à maintenir.
Ce qu’il faut garder en tête avant de brancher un enfant sur son parent
- output() est l’API recommandée pour les nouveaux composants Angular.
- @Output() + EventEmitter reste pleinement supporté dans les bases existantes.
- Un output ne remonte pas dans le DOM : le parent doit l’écouter explicitement.
- L’événement peut transporter une valeur simple ou un objet plus riche via $event.
- Si tu synchronises une valeur plutôt qu’un événement ponctuel, model() peut être plus adapté.
Pourquoi cette communication existe
Le problème à résoudre est simple : l’enfant sait qu’un fait s’est produit, mais il ne doit pas connaître les détails du parent qui va l’exploiter. Je préfère penser à ce mécanisme comme à un contrat d’émission clair : l’enfant annonce un événement, le parent décide quoi en faire. C’est beaucoup plus propre que de laisser un composant enfant appeler directement une méthode du parent ou manipuler un état partagé sans cadrage.
Dans Angular, cette logique complète bien le flux classique des inputs : les données descendent vers l’enfant, les événements remontent vers le parent. La documentation Angular rappelle aussi que ces événements personnalisés ne remontent pas dans le DOM, donc il ne faut pas compter sur une propagation implicite comme avec certains événements natifs. Une fois ce cadre posé, l’implémentation devient beaucoup plus lisible, et on peut passer à la mécanique concrète.
Comment ça circule entre un composant enfant et son parent
La version moderne consiste à déclarer une sortie avec output(), puis à appeler emit() quand quelque chose mérite d’être signalé. Le parent, lui, écoute cette sortie avec la syntaxe de liaison d’événement dans le template. Angular transforme alors cet échange en contrat typé, ce qui aide énormément dès qu’on commence à transmettre autre chose qu’un simple booléen.
import { Component, output } from '@angular/core';
@Component({
selector: 'app-child',
template: `
`,
})
export class ChildComponent {
saved = output<{ id: number; label: string }>();
save() {
this.saved.emit({ id: 7, label: 'Brouillon' });
}
}@Component({
selector: 'app-parent',
template: `
`,
})
export class ParentComponent {
onSaved(payload: { id: number; label: string }) {
console.log(payload);
}
}Ce qui compte ici, ce n’est pas seulement la syntaxe. C’est le fait que le composant enfant reste autonome, tandis que le parent conserve la responsabilité du traitement. Angular indique aussi que le nom d’un output est sensible à la casse, et qu’un output peut être renommé via un alias si c’est utile pour éviter un conflit avec un événement natif. Pour une création dynamique de composant, on peut même s’abonner au résultat de façon programmatique, ce qui reste pratique dans des cas plus avancés. Avec cette base, la vraie question devient maintenant : faut-il écrire ce lien avec output() ou avec l’ancien couple @Output() et EventEmitter ?
output() ou @Output() + EventEmitter
La documentation Angular actuelle recommande output() pour les nouveaux projets, et ce choix n’est pas cosmétique. L’API est plus directe, plus cohérente avec l’écosystème moderne d’Angular, et elle évite un peu de bruit autour de la déclaration des sorties. En face, @Output() + EventEmitter reste totalement supporté, donc il n’y a aucune urgence à réécrire tout un codebase stable juste pour suivre une mode.
| Approche | Ce que j’en pense | Quand l’utiliser | Point de vigilance |
|---|---|---|---|
| output() | Plus moderne, plus lisible, et mieux alignée avec Angular récent. | Nouveaux composants, refactor ciblé, codebase que tu veux garder nette. | Doit être appelé dans un initialiseur de propriété, pas ailleurs. |
| @Output() + EventEmitter | Parfaitement valide, surtout dans un projet déjà structuré autour de cette syntaxe. | Migration progressive, maintenance, coexistence avec du code existant. | EventEmitter s’appuie sur RxJS Subject, donc il ne faut pas le traiter comme un simple bus global. |
Je résume souvent ce choix de façon très concrète : si tu écris du neuf, pars sur output(). Si tu maintiens de l’existant, garde @Output() quand il est déjà partout, puis migre progressivement si cela a un vrai intérêt. Cette logique évite les réécritures décoratives et garde le projet cohérent. Reste maintenant à voir dans quels cas un simple output suffit, et quand une liaison plus forte avec model() est plus pertinente.
Quand un output suffit et quand model() est plus juste
Un output est très bien quand l’enfant signale un événement ponctuel : un clic, une suppression, une validation, une fermeture de panneau, une sélection de ligne. Dans ces cas-là, je veux que le parent soit informé, pas que l’enfant devienne responsable de l’état global. C’est la bonne frontière entre notification et synchronisation.
En revanche, si tu construis un composant dont la valeur doit rester synchronisée entre parent et enfant, Angular propose aujourd’hui model() pour les scénarios de liaison bidirectionnelle. La documentation officielle montre ce pattern pour les composants de type compteur, où l’enfant expose une valeur et le parent la lie avec la syntaxe [(...)]. Je considère cela comme un meilleur choix qu’un empilement d’outputs artificiels quand on veut simplement maintenir une valeur partagée proprement.
- Output simple : fermeture de modal, clic sur bouton, suppression d’un élément.
- Output enrichi : envoi d’un objet avec identifiant, libellé et métadonnées.
- model() : champ de formulaire, compteur, valeur que le parent et l’enfant doivent garder synchronisée.
Cette distinction compte beaucoup dans les interfaces complexes, car elle évite les composants qui mélangent événement, état et logique métier dans le même flux. Et une fois cette frontière claire, il reste à sécuriser l’implémentation contre les erreurs les plus fréquentes.
Les pièges qui cassent souvent l’échange
Le premier piège que je vois souvent, c’est l’usage de output() hors d’un initialiseur de propriété. Angular le refuse, et ce n’est pas une coquetterie de syntaxe : le compilateur doit pouvoir reconnaître les sorties au moment de l’analyse statique. Le second piège, c’est d’attendre un comportement de propagation du DOM. Un output n’est pas un événement HTML standard, donc il doit être capté directement par le composant parent.
Il y a aussi quelques erreurs plus discrètes mais tout aussi pénibles :
- Nommer une sortie comme un événement natif et créer une ambiguïté inutile.
- Oublier que les noms sont sensible à la casse.
- Envoyer un payload trop gros alors que le parent n’a besoin que d’un identifiant.
- Utiliser EventEmitter comme si c’était une source d’état partagée, alors que ce n’est pas son rôle.
- Changer le nom dans le template avec un alias sans mettre à jour la lecture du code TypeScript.
Je rappelle aussi un détail utile pour les architectures par héritage : les outputs peuvent être hérités par une classe enfant, ce qui est pratique dans les composants de base réutilisés. Quand on garde ces contraintes en tête, le mécanisme devient très fiable au lieu de se transformer en source de bugs intermittents. Il ne reste plus qu’à aligner tout cela avec une manière de coder qui garde les composants simples à faire évoluer.
Ce que je retiens pour écrire des composants plus nets
Si je devais résumer la pratique en une règle simple, je dirais ceci : un output sert à signaler une action, pas à cacher de la logique métier. Plus le payload est petit et explicite, plus le composant reste facile à tester et à faire évoluer. Plus le nom de l’événement décrit l’action réelle, plus le code du parent se lit vite.
Dans un projet Angular moderne, je pars donc généralement sur output(), j’utilise @Output() + EventEmitter quand je maintiens du code déjà structuré comme ça, et je réserve model() aux cas où la valeur doit être synchronisée dans les deux sens. Si je dois migrer une base ancienne, je le fais par zones cohérentes plutôt qu’en big bang, ce qui limite les régressions et laisse le code plus homogène après coup. C’est la voie la plus simple pour garder des composants frontend lisibles, testables et franchement plus agréables à maintenir.