À propos de ce cours
Une sortie déclarée avec output() permet à l’enfant d’annoncer un événement avec emit(). Le parent l’écoute avec (nom)="…" et reçoit la valeur dans $event, typée et vérifiée à la compilation. Les données descendent par les entrées et les événements remontent par les sorties, mais d’un seul niveau : il faut les relayer. Le cours montre aussi comment bien nommer une sortie, s’y abonner par le code (subscribe, outputToObservable, outputFromObservable) et former le couple valeur / valeurChange pour [( )]. Il présente l’ancienne écriture @Output() + EventEmitter avec sa migration, puis se termine par 5 activités corrigées et les erreurs fréquentes.
-
Objectifs
- Déclarer une sortie avec
output<T>()et émettre un événement avecemit() - Écouter une sortie dans le parent avec
(nom)="…"et exploiter$event - Choisir un nom de sortie sans piège et utiliser un alias
- Faire remonter un événement sur plusieurs niveaux en le relayant, tout en laissant les données au composant qui les possède
- S'abonner à une sortie par le code et faire le lien avec RxJS (
outputToObservable,outputFromObservable) - Créer le couple
valeur/valeurChangequi rend possible[(valeur)] - Lire et migrer l'ancienne écriture
@Output()+EventEmitter
- Déclarer une sortie avec
-
Présentation
- Dans « Transmettre des données avec input() », les données descendaient du parent vers l'enfant. Il faut maintenant le chemin inverse : l'enfant doit pouvoir prévenir son parent qu'il s'est passé quelque chose (un clic, un choix, une validation).
- C'est le rôle des sorties (outputs). L'enfant n'agit pas lui-même sur les données du parent : il annonce un événement, éventuellement accompagné d'une valeur, et le parent décide quoi en faire.
- Depuis Angular 17.3, on déclare une sortie avec la fonction
output(). L'ancienne écriture,@Output()avec unEventEmitter, reste très présente dans les projets existants : nous verrons les deux.

- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/sorties/. Tous les exemples ont été testés avec Angular 22 (composantsOnPushpar défaut, sans Zone.js).
-
Déclarer, émettre et écouter une sortie
- Dans l'enfant, une sortie est une propriété initialisée avec
output<T>(), importée depuis@angular/core. Le typeTest celui de la valeur transportée ;output<void>()ne transporte rien. - Pour déclencher l'événement, l'enfant appelle
emit(valeur), depuis son template ou depuis une méthode. - Dans le parent, on écoute la sortie exactement comme un événement du navigateur :
(voter)="compter($event)". La variable$eventcontient la valeur passée àemit().
- Dans l'enfant, une sortie est une propriété initialisée avec
import { Component, output, signal } from '@angular/core';
type Vote = 'pour' | 'contre';
// Composant enfant : il annonce ce que l'utilisateur a fait, sans rien compter lui-même
@Component({
selector: 'app-boutons-vote',
template: `
<button (click)="voter.emit('pour')">Pour</button>
<button (click)="voter.emit('contre')">Contre</button>
<button (click)="effacer.emit()">Effacer</button>
`,
})
export class BoutonsVote {
readonly voter = output<Vote>(); // la sortie transporte une valeur : 'pour' ou 'contre'
readonly effacer = output<void>(); // la sortie ne transporte rien
}
// Composant parent : il écoute les sorties et décide quoi faire
@Component({
selector: 'app-premiere-sortie',
imports: [BoutonsVote],
template: `
<app-boutons-vote (voter)="compter($event)" (effacer)="remettreAZero()" />
<p>Pour : {{ pour() }} · Contre : {{ contre() }} · dernier vote : {{ dernier() }}</p>
`,
})
export class PremiereSortie {
protected readonly pour = signal(0);
protected readonly contre = signal(0);
protected readonly dernier = signal('aucun');
protected compter(vote: Vote): void {
if (vote === 'pour') this.pour.update((n) => n + 1);
else this.contre.update((n) => n + 1);
this.dernier.set(vote);
}
protected remettreAZero(): void {
this.pour.set(0);
this.contre.set(0);
this.dernier.set('aucun');
}
}
Pour : 0 · Contre : 0 · dernier vote : aucun
(Pour, Pour, Contre) Pour : 2 · Contre : 1 · dernier vote : contre
(Effacer) Pour : 0 · Contre : 0 · dernier vote : aucun
BoutonsVotene compte rien : il ne possède aucune donnée. Le même composant pourrait servir à un sondage, à des commentaires ou à un quiz ; c'est le parent qui donne un sens au vote.$eventest typéVote:compter($event)est vérifié par le compilateur. Avecoutput<void>(), on écrit simplementemit()et le parent n'utilise pas$event.- Une sortie est déclarée
readonly: on ne remplace jamais l'objet, on l'utilise seulement pour émettre. - Attention :
output()ne peut être appelée que dans l'initialisation d'une propriété de classe. Ailleurs (dans une méthode, par exemple), le compilateur refuse :NG8110: Unsupported call to the output function. This function can only be called in the initializer of a class member.

-
Transmettre une valeur avec
$event- Une sortie peut transporter n'importe quel type : nombre, texte, union, objet. L'objet est souvent le plus pratique, car il regroupe tout ce que le parent doit savoir.
- Dans le parent,
$eventpeut être combiné à d'autres arguments. Le même composant peut ainsi être utilisé plusieurs fois, pour des usages différents.
import { Component, input, output, signal } from '@angular/core';
export interface Couleur {
nom: string;
hex: string;
}
// Composant enfant réutilisable : il émet un objet complet
@Component({
selector: 'app-palette',
template: `
<span class="titre">{{ titre() }}</span>
@for (c of couleurs; track c.hex) {
<button [style.background]="c.hex" [title]="c.nom" (click)="choisir.emit(c)"></button>
}
`,
styles: `:host { display: block; margin: 6px 0; } .titre { display: inline-block; width: 60px; }
button { width: 26px; height: 26px; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 0 1px #94a3b8; margin-right: 4px; cursor: pointer; }`,
})
export class Palette {
readonly titre = input('Couleur');
readonly choisir = output<Couleur>();
protected readonly couleurs: Couleur[] = [
{ nom: 'Bleu', hex: '#3178c6' }, { nom: 'Rouge', hex: '#dd0031' },
{ nom: 'Vert', hex: '#16a34a' }, { nom: 'Blanc', hex: '#ffffff' }, { nom: 'Noir', hex: '#0f172a' },
];
}
// Composant parent : le même enfant, utilisé deux fois, pour deux usages différents
@Component({
selector: 'app-charge-utile',
imports: [Palette],
template: `
<app-palette titre="Fond" (choisir)="appliquer($event, 'fond')" />
<app-palette titre="Texte" (choisir)="appliquer($event, 'texte')" />
<p class="apercu" [style.background]="fond().hex" [style.color]="texte().hex">
Texte {{ texte().nom }} sur fond {{ fond().nom }}
</p>
`,
styles: `.apercu { padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; }`,
})
export class ChargeUtile {
protected readonly fond = signal<Couleur>({ nom: 'Blanc', hex: '#ffffff' });
protected readonly texte = signal<Couleur>({ nom: 'Noir', hex: '#0f172a' });
protected appliquer(c: Couleur, cible: 'fond' | 'texte'): void {
(cible === 'fond' ? this.fond : this.texte).set(c);
}
}
Texte Noir sur fond Blanc
(Fond : bleu, Texte : blanc) Texte Blanc sur fond Bleu ← l'aperçu change de couleurs
choisir.emit(c)envoie l'objetCouleurcomplet ; le parent reçoit$eventde typeCouleur.appliquer($event, 'fond')ajoute une information que seul le parent connaît : à quoi sert cette palette.- Si le gestionnaire attend un autre type, le template est refusé :
(noter)="afficher($event)"avecafficher(t: string)et une sortieoutput<number>()donneTS2345: Argument of type 'number' is not assignable to parameter of type 'string'. - Oublier
$eventalors que la méthode attend un argument donneTS2554: Expected 1 arguments, but got 0.

- Attention : une faute de frappe dans le nom d'une sortie (
(notr)au lieu de(noter)) ne provoque aucune erreur. Angular croit écouter un événement du navigateur qui n'arrive jamais : le gestionnaire n'est simplement jamais appelé.
-
Bien nommer une sortie
- Choisissez un verbe métier:
voter,choisir,supprimer,valider. Le guide de style d'Angular déconseille le préfixeon(onVoter). - Ne donnez jamais à une sortie le nom d'un événement du navigateur (
click,change,input,submit) : l'exemple suivant montre pourquoi. - Comme pour les entrées, une option
aliasdonne un nom public différent du nom interne.
- Choisissez un verbe métier:
import { Component, output, signal } from '@angular/core';
// À NE PAS FAIRE : une sortie qui porte le nom d'un événement du navigateur
@Component({
selector: 'app-bouton-piege',
template: `<button (click)="click.emit('sortie')">Bouton piégé</button>`,
})
export class BoutonPiege {
readonly click = output<string>();
}
// La bonne pratique : un nom métier, et éventuellement un alias public
@Component({
selector: 'app-bouton-sur',
template: `<button (click)="appui.emit('sortie')">Bouton sûr</button>`,
})
export class BoutonSur {
readonly appui = output<string>({ alias: 'valider' }); // nom interne : appui, nom public : valider
}
@Component({
selector: 'app-nommer-sortie',
imports: [BoutonPiege, BoutonSur],
template: `
<app-bouton-piege (click)="noter('piégé', $event)" />
<app-bouton-sur (valider)="noter('sûr', $event)" />
<button (click)="journal.set([])">Vider</button>
<ol>
@for (ligne of journal(); track $index) { <li>{{ ligne }}</li> }
</ol>
`,
})
export class NommerSortie {
protected readonly journal = signal<string[]>([]);
protected noter(bouton: string, e: unknown): void {
const recu = e instanceof Event ? `${e.constructor.name} du navigateur` : `sortie « ${e} »`;
this.journal.update((j) => [...j, `${bouton} : ${recu}`]);
}
}
(clic sur « Bouton piégé »)
1. piégé : sortie « sortie »
2. piégé : PointerEvent du navigateur ← le gestionnaire est appelé DEUX fois
(clic sur « Bouton sûr »)
3. sûr : sortie « sortie »
- Le clic natif sur le
<button>intérieur remonte (bouillonnement du DOM) jusqu'à la balise<app-bouton-piege>. Le parent reçoit donc la sortie et l'événement du navigateur, avec deux$eventde types différents. output({ alias: 'valider' }): la classe utilisethis.appui, le parent écoute(valider). Réservez l'alias aux cas où le nom public est imposé ; sinon, un seul nom est plus lisible.

-
Remonter un événement sur plusieurs niveaux
- Contrairement aux événements du navigateur, une sortie Angular ne bouillonne pas: seul le parent direct peut l'écouter. Pour atteindre un composant plus haut, chaque niveau intermédiaire doit relayer l'événement avec sa propre sortie.
- Ce schéma s'accompagne d'une règle d'or : les données descendent, les événements remontent. Un seul composant possède les données ; c'est le seul à les modifier.
import { Component, computed, input, output, signal } from '@angular/core';
export interface Tache {
id: number;
titre: string;
faite: boolean;
}
// Petit-enfant : une ligne. Il ne connaît que SA tâche
@Component({
selector: 'app-item-tache',
template: `
<label [class.faite]="tache().faite">
<input type="checkbox" [checked]="tache().faite" (change)="basculer.emit()" />
{{ tache().titre }}
</label>
<button (click)="supprimer.emit()">✕</button>
`,
styles: `:host { display: flex; justify-content: space-between; max-width: 320px; } .faite { text-decoration: line-through; color: #94a3b8; }`,
})
export class ItemTache {
readonly tache = input.required<Tache>();
readonly basculer = output<void>();
readonly supprimer = output<void>();
}
// Enfant : la liste. Il RELAIE les événements en ajoutant l'identifiant
@Component({
selector: 'app-liste-taches',
imports: [ItemTache],
template: `
@for (t of taches(); track t.id) {
<app-item-tache [tache]="t" (basculer)="basculer.emit(t.id)" (supprimer)="supprimer.emit(t.id)" />
} @empty {
<p>Rien à faire.</p>
}
`,
})
export class ListeTaches {
readonly taches = input.required<Tache[]>();
readonly basculer = output<number>();
readonly supprimer = output<number>();
}
// Parent : le SEUL à posséder et à modifier les données
@Component({
selector: 'app-remonter-etat',
imports: [ListeTaches],
template: `
<app-liste-taches [taches]="taches()" (basculer)="basculer($event)" (supprimer)="supprimer($event)" />
<p>{{ faites() }} / {{ taches().length }} terminée(s)</p>
`,
})
export class RemonterEtat {
protected readonly taches = signal<Tache[]>([
{ id: 1, titre: 'Lire le chapitre', faite: true },
{ id: 2, titre: 'Faire les activités', faite: false },
{ id: 3, titre: 'Rendre le TP', faite: false },
]);
protected readonly faites = computed(() => this.taches().filter((t) => t.faite).length);
protected basculer(id: number): void {
this.taches.update((l) => l.map((t) => (t.id === id ? { ...t, faite: !t.faite } : t)));
}
protected supprimer(id: number): void {
this.taches.update((l) => l.filter((t) => t.id !== id));
}
}
Lire le chapitre (barré) · Faire les activités · Rendre le TP 1 / 3 terminée(s)
(case « Faire les activités ») 2 / 3 terminée(s)
(✕ sur « Lire le chapitre ») Faire les activités · Rendre le TP 1 / 2 terminée(s)

ItemTacheémetbasculersans valeur : il ne connaît que sa tâche.ListeTachesrelaie en ajoutant l'identifiant (basculer.emit(t.id)) : c'est elle qui sait de quelle tâche il s'agit.- Seul
RemonterEtatmodifie le signaltaches, toujours avec un nouveau tableau (map,filter) : les entrées des enfants sont alors bien mises à jour. - Quand il faut traverser beaucoup de niveaux, relayer devient lourd : on utilise alors un service partagé (voir « La communication entre composants Angular »).
-
S'abonner à une sortie par le code
- Le plus souvent, on écoute une sortie dans le template. Mais
output()renvoie un objetOutputEmitterRefqui possède aussi une méthodesubscribe(): un parent peut s'y abonner par le code, après avoir obtenu l'enfant avecviewChild(). - La fonction
outputToObservable()(paquet@angular/core/rxjs-interop) transforme une sortie enObservableRxJS, pour utiliser ses opérateurs (filter,map,debounceTime…). À l'inverse,outputFromObservable()crée une sortie à partir d'unObservable(activité 4).
- Le plus souvent, on écoute une sortie dans le template. Mais
import { Component, DestroyRef, afterNextRender, inject, output, signal, viewChild } from '@angular/core';
import { outputToObservable } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs';
// Enfant : un compte à rebours qui émet à chaque seconde, puis à la fin
@Component({
selector: 'app-minuteur',
template: `<button (click)="demarrer()" [disabled]="reste() > 0">Démarrer</button> <strong>{{ reste() }} s</strong>`,
})
export class Minuteur {
readonly tic = output<number>();
readonly termine = output<void>();
protected readonly reste = signal(0);
private id?: ReturnType<typeof setInterval>;
constructor() {
inject(DestroyRef).onDestroy(() => clearInterval(this.id)); // arrêter si le composant disparaît
}
protected demarrer(): void {
this.reste.set(4);
this.id = setInterval(() => {
this.reste.update((r) => r - 1);
this.tic.emit(this.reste());
if (this.reste() === 0) {
clearInterval(this.id);
this.termine.emit();
}
}, 1000);
}
}
// Parent : il s'abonne aux sorties par le code, et non dans le template
@Component({
selector: 'app-abonnement-code',
imports: [Minuteur],
template: `<app-minuteur /> <p>{{ journal().join(' · ') }}</p>`,
})
export class AbonnementCode {
private readonly minuteur = viewChild.required(Minuteur);
protected readonly journal = signal<string[]>([]);
constructor() {
afterNextRender(() => {
// 1. subscribe() : désabonnement automatique quand le minuteur est détruit
this.minuteur().termine.subscribe(() => this.noter('terminé !'));
// 2. outputToObservable() : la sortie devient un Observable RxJS
outputToObservable(this.minuteur().tic)
.pipe(filter((r) => r % 2 === 0))
.subscribe((r) => this.noter(`${r} s (pair)`));
});
}
private noter(texte: string): void {
this.journal.update((j) => [...j, texte]);
}
}
(Démarrer) 4 s … 3 s … 2 s … 1 s … 0 s
2 s (pair) · 0 s (pair) · terminé !
afterNextRenderattend que la vue soit affichée : avant, l'enfant n'existe pas encore etviewChild.requiredne peut pas le fournir.- Les abonnements faits avec
subscribe()sont automatiquement supprimés quand le composant enfant est détruit : pas de fuite de mémoire. - Inversement, émettre depuis un composant déjà détruit (par exemple dans un
setTimeoutoublié) provoque une erreur à l'exécution :NG0953: Unexpected emit for destroyed `OutputRef`. The owning directive/component is destroyed.D'où leDestroyRef.onDestroyqui arrête le minuteur. - Cette technique reste exceptionnelle : écouter dans le template est plus simple et plus lisible.
-
Le couple
valeur/valeurChangeet[( )]- Une entrée
actifassociée à une sortie nommée exactementactifChangeforme un couple reconnu par Angular : le parent peut alors écrire la liaison bidirectionnelle[(actif)]="wifi". - C'est ce mécanisme que la fonction
model(), vue dans « Le two-way data binding et ngModel », crée en une seule ligne.
- Une entrée
import { Component, input, output, signal } from '@angular/core';
// Une entrée « actif » + une sortie « actifChange » : le couple permet [(actif)]
@Component({
selector: 'app-interrupteur',
template: `
<button role="switch" [class.on]="actif()" (click)="actifChange.emit(!actif())">
{{ actif() ? 'Activé' : 'Désactivé' }}
</button>
`,
styles: `button { border-radius: 14px; padding: 4px 14px; border: 1px solid #94a3b8; } .on { background: #16a34a; color: #fff; }`,
})
export class Interrupteur {
readonly actif = input(false);
readonly actifChange = output<boolean>();
}
@Component({
selector: 'app-sortie-change',
imports: [Interrupteur],
template: `
<p>Écriture longue : <app-interrupteur [actif]="wifi()" (actifChange)="wifi.set($event)" /></p>
<p>Écriture courte : <app-interrupteur [(actif)]="wifi" /></p>
<p>Wi-Fi : {{ wifi() ? 'oui' : 'non' }}</p>
`,
})
export class SortieChange {
protected readonly wifi = signal(false);
}
Écriture longue : Désactivé · Écriture courte : Désactivé · Wi-Fi : non
(clic sur le 1er interrupteur) Activé · Activé · Wi-Fi : oui
(clic sur le 2e interrupteur) Désactivé · Désactivé · Wi-Fi : non
[(actif)]="wifi"est un raccourci pour[actif]="wifi()" (actifChange)="wifi.set($event)". Les deux interrupteurs partagent le même signal : ils restent synchronisés.- L'interrupteur ne modifie pas son entrée (c'est impossible) : il propose la nouvelle valeur par sa sortie, et le parent l'accepte.
- Avec
readonly actif = model(false);, l'enfant pourrait écrirethis.actif.set(…)directement. Utilisezmodel()pour un vrai champ de saisie, et le coupleinput/outputquand l'enfant doit seulement proposer une valeur.
-
L'ancienne écriture :
@Output()etEventEmitter- Avant
output(), une sortie était une propriété décorée par@Output()(voir « Les décorateurs Angular ») et initialisée avec unnew EventEmitter<T>(). Le premier argument de@Output()est l'alias. - Côté parent, rien ne change :
(valeurChange),(reinitialiser)et[(valeur)]s'écrivent de la même façon.
- Avant
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
// L'ancienne écriture : @Output() + EventEmitter
@Component({
selector: 'app-selecteur-ancien',
template: `
@for (o of options; track o) {
<button [class.actif]="o === valeur" (click)="choisir(o)">{{ o }}</button>
}
<button (click)="vider.emit()">Aucun</button>
`,
styles: `.actif { background: #3178c6; color: #fff; }`,
})
export class SelecteurAncien {
@Input() options: string[] = [];
@Input() valeur = '';
@Output() valeurChange = new EventEmitter<string>();
@Output('reinitialiser') vider = new EventEmitter<void>(); // alias : nom public « reinitialiser »
protected choisir(o: string): void {
this.valeurChange.emit(o);
}
}
@Component({
selector: 'app-ancienne-ecriture',
imports: [SelecteurAncien],
template: `
<app-selecteur-ancien [options]="tailles" [(valeur)]="taille" (reinitialiser)="taille = ''" />
<p>Taille choisie : {{ taille || 'aucune' }}</p>
`,
})
export class AncienneEcriture {
protected readonly tailles = ['S', 'M', 'L', 'XL'];
protected taille = 'M';
}
Taille choisie : M (bouton M en bleu)
(XL) Taille choisie : XL
(Aucun) Taille choisie : aucune

EventEmitterhérite deSubject(RxJS) : il apporte toute la bibliothèque RxJS, alors queoutput()n'en a pas besoin.- Sa méthode est déclarée
emit(value?: T): l'argument est facultatif.valeurChange.emit()sans valeur passe donc la compilation et envoieundefined. Avecoutput<number>(), la même erreur est détectée :TS2554: Expected 1 arguments, but got 0. - Rien n'empêche de remplacer l'
EventEmitterou de lui ajouter des abonnés à la main ; avecoutput(), le seul usage possible estemit()(écrirethis.noter.set(3)donneTS2339: Property 'set' does not exist on type 'OutputEmitterRef<number>'.). - Angular fournit une migration automatique :
ng generate @angular/core:output-migration.
-
Aide-mémoire

-
Activité 1 : une notation par étoiles
- Créer le composant
app-notation-etoiles:- entrées
note(0 par défaut) etmax(5 par défaut), avecnumberAttribute; - entrée
lectureSeuleavecbooleanAttribute, qui désactive les étoiles ; - sortie
noter = output<number>(), émise au clic sur une étoile ; - au survol, les étoiles se remplissent jusqu'à l'étoile pointée (prévisualisation avec un signal
survol), sans rien émettre.
- entrées
- Le parent
app-avis-produitsaffiche trois produits (Clavier 4, Souris 0, Écran 3), met à jour la note du produit cliqué (de façon immuable) et affiche une ligne « Moyenne » avec une notation en lecture seule et « Dernière action : Souris noté 5 / 5 ». Les produits sans note ne comptent pas dans la moyenne.
- Créer le composant

- Vérifier : au départ la moyenne vaut 3.5 ; après Souris = 5 elle vaut 4.0 ; après Clavier = 2 elle vaut 3.3.
- Pourquoi la notation ne modifie-t-elle pas elle-même sa note ? Que faudrait-il changer pour pouvoir écrire
[(note)]?
-
Activité 2 : un digicode
- Créer
app-pave-numerique: 10 chiffres, une touche C et une touche OK. Le composant ne stocke rien: il émettouche(le chiffre, en texte),effaceretvalider(sans valeur). Une entréebloquedésactive toutes les touches. - Le parent
app-digicodepossède la saisie (4 chiffres au plus, affichés masqués••__) et vérifie le code secret2026:- moins de 4 chiffres : « Le code doit comporter 4 chiffres. » ;
- code faux : « Code faux (1 / 3). », en rouge ; après 3 erreurs, « Clavier bloqué : 3 erreurs. » et le pavé est bloqué ;
- bon code : « Porte ouverte. », en vert, et le pavé est bloqué.
- Créer

- Pourquoi le code secret ne doit-il pas être placé dans le pavé numérique ?
- Réutiliser le même pavé dans une calculatrice : qu'est-ce qui change, et qu'est-ce qui reste identique ?
-
Activité 3 : une boîte de confirmation
- Créer
app-boite-confirmation: entrée obligatoiremessage, entréelibelleConfirmer(« Confirmer » par défaut), sortiesconfirmeretannuler(sans valeur). La touche Échap émet aussiannuler(écouteurdocument:keydown.escapedanshost). - Le parent
app-fichiers-confirmationaffiche une liste de fichiers avec un bouton « Supprimer ». Le clic ne supprime pas : il ouvre la boîte (« Supprimer « notes.txt » définitivement ? »). Les boutons « Supprimer » de la liste sont désactivés tant que la boîte est ouverte. - Confirmer retire le fichier et affiche « « rapport.pdf » supprimé. » ; Annuler ou Échap ferme la boîte et affiche « Suppression de « notes.txt » annulée. ».
- Créer

- Comment le parent sait-il quel fichier supprimer, alors que
confirmern'émet aucune valeur ? - Réutiliser la boîte pour « Vider le dossier » avec
libelleConfirmer="Tout vider".
-
Activité 4 : une recherche avec délai
- Créer
app-champ-recherche: un champ et un bouton « Effacer ». À chaque frappe, le texte est poussé dans unSubject<string>. La sortierechercherest créée avecoutputFromObservable(): elle n'émet qu'après 400 ms sans frappe (debounceTime), un texte nettoyé (trim, minuscules) et jamais deux fois le même terme (distinctUntilChanged). - Deux autres sorties :
touche(à chaque frappe) eteffacer. - Le parent
app-recherche-delaifiltre une liste de 8 chapitres du cours et affiche « Touches : 5 · recherches émises : 1 · terme : « les s » ».
- Créer

- Taper « les s » rapidement : combien de touches, combien de recherches ? Effacer le dernier « s » et le retaper : pourquoi aucune nouvelle recherche n'est-elle émise ?
- Taper « ÉVÉN » : quels chapitres s'affichent ?
-
Activité 5 : migrer
@Output()versoutput()- On vous fournit le compteur ci-dessous, écrit avec l'ancienne syntaxe :
@Component({
selector: 'app-compteur-ancien',
template: `<button (click)="changer(-pas)">−</button> {{ valeur }} <button (click)="changer(pas)">+</button>`,
})
export class CompteurAncien {
@Input() valeur = 0;
@Input({ transform: numberAttribute }) pas = 1;
@Input({ transform: numberAttribute }) max = 10;
@Output() valeurChange = new EventEmitter<number>();
@Output('limite') limiteAtteinte = new EventEmitter<void>();
changer(delta: number): void {
const v = Math.min(this.max, Math.max(0, this.valeur + delta));
this.valeurChange.emit(v);
if (v === this.max && v !== this.valeur) this.limiteAtteinte.emit(); // seulement en atteignant la limite
}
}
- Écrire
app-compteur, son équivalent avecinput()etoutput()(en gardant l'alias publiclimite). - Afficher les deux versions dans un tableau, liées aux mêmes signaux avec
[(valeur)]: « Places » (3, max 5) et « Budget » (50, pas 25, max 100). Les deux colonnes doivent toujours afficher la même valeur, et chaque limite atteinte est notée (« ancien » ou « nouveau »).

- Que change la migration côté parent ? Et pour la vérification de
emit()? - Lancer
ng generate @angular/core:output-migrationsur une copie deCompteurAncienet comparer avec votre version.
- Les corrigés des cinq activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier
corriges/).
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
TS2554: Expected 1 arguments, but got 0. (sur emit()) |
emit() sans valeur sur une sortie typée |
Passer la valeur, ou déclarer output<void>() |
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. |
Valeur émise du mauvais type | Émettre une valeur du type de output<T>() |
TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. |
$event passé à une méthode qui attend un autre type |
Accorder le type du paramètre et celui de la sortie |
TS2554: Expected 1 arguments, but got 0. (dans le template) |
$event oublié : (voter)="compter()" |
(voter)="compter($event)" |
TS2339: Property 'set' does not exist on type 'OutputEmitterRef<number>'. |
Sortie utilisée comme un signal | Utiliser emit() |
NG8110: Unsupported call to the output function. This function can only be called in the initializer of a class member. |
output() appelée dans une méthode ou le constructeur |
La déclarer comme propriété de classe |
NG8001: 'app-boutons-vote' is not a known element |
Composant enfant non importé | L'ajouter dans imports du parent |
NG0953: Unexpected emit for destroyed `OutputRef`. |
Émission après la destruction (minuteur, requête) | Arrêter le traitement dans DestroyRef.onDestroy |
| Le gestionnaire n'est jamais appelé, sans erreur | Faute de frappe dans le nom : (notr) |
Vérifier le nom (ou l'alias) de la sortie |
| Le gestionnaire est appelé deux fois | Sortie nommée click, change… comme un événement DOM |
Lui donner un nom métier |
| Le grand-parent ne reçoit rien | Les sorties ne bouillonnent pas | Relayer à chaque niveau, ou utiliser un service |
emit() envoie undefined sans erreur |
EventEmitter.emit(value?: T): argument facultatif |
Migrer vers output() |
-
À retenir
output<T>()déclare une sortie ; l'enfant émet avecemit(valeur);output<void>()n'émet aucune valeur.- Le parent écoute avec
(nom)="instruction";$eventcontient la valeur émise, typée et vérifiée par le compilateur. - L'enfant annonce, le parent décide : les données descendent par les entrées, les événements remontent par les sorties.
- Une sortie ne remonte que d'un niveau : il faut la relayer, ou passer par un service.
- Un nom métier, jamais un événement du navigateur ;
aliasseulement si nécessaire. valeur+valeurChangepermet[(valeur)];model()en est le raccourci.subscribe(),outputToObservable()etoutputFromObservable()font le lien avec le code et RxJS.@Output()+EventEmitterest l'ancienne écriture : à connaître, à migrer versoutput().
Un bon composant enfant ne sait pas à quoi il sert : il affiche ce qu'on lui donne et annonce ce qui se passe. C'est ce qui le rend réutilisable.