À propos de ce cours
Apprenez à faire réagir votre application Angular à chaque action de l’utilisateur. Vous écouterez les événements du navigateur, gérerez le clavier et ses raccourcis, maîtriserez la propagation et preventDefault(), écouterez la fenêtre et le document, puis créerez vos propres événements avec output().
-
Objectifs
- Écouter les événements du navigateur avec la liaison d'événement
(evenement)="instruction"et exploiter l'objet$event - Réagir au clavier avec les filtres de touches et créer des raccourcis
- Comprendre la propagation d'un événement et maîtriser
stopPropagation()etpreventDefault() - Écouter la fenêtre, le document et l'élément hôte avec la propriété
host - Créer ses propres événements avec
output()pour faire communiquer un composant enfant avec son parent - Écouter un événement par programme (
Renderer2,fromEvent) sans oublier la mise à jour de l'affichage
- Écouter les événements du navigateur avec la liaison d'événement
-
Présentation
- Une application réagit sans arrêt à ce que fait l'utilisateur : un clic, une frappe au clavier, un survol, un glisser-déposer, un redimensionnement de la fenêtre… Chacune de ces actions produit un événement du navigateur.
- Dans « Le data binding dans Angular », vous avez découvert la liaison d'événement
( ). Ce chapitre va plus loin : types d'événements, clavier, propagation, événements globaux, événements personnalisés entre composants et écoute par programme. - Avec Angular 22 (sans
zone.js,OnPushpar défaut), un événement écouté dans le template déclenche automatiquement la mise à jour du composant. Les données qui changent restent stockées dans des signals (voir « Les signals dans Angular »).

- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/evenements/. Tous les exemples ont été testés avec Angular 22.
-
Écouter un événement et lire
$event- La syntaxe
(evenement)="instruction"place un écouteur sur l'élément. Le nom entre parenthèses est celui de l'événement DOM, sans le préfixeon:(click)et non(onclick). - L'instruction est le plus souvent un appel de méthode. Elle peut recevoir
$event, l'objet créé par le navigateur, ou une variable de référence (#champ) pour lire directement la valeur d'un élément.
- La syntaxe
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-bases-evenements',
template: `
<button (click)="onClic($event)" (dblclick)="noter('double-clic')">Cliquez-moi</button>
<input #champ (input)="saisie.set(champ.value)" (focus)="noter('focus')" (blur)="noter('blur')" placeholder="Votre nom" />
<input (input)="onSaisie($event)" placeholder="Ville" />
<select (change)="niveau.set($any($event.target).value)">
<option>Débutant</option><option>Avancé</option>
</select>
<div class="survol" (mouseenter)="survol.set(true)" (mouseleave)="survol.set(false)">
{{ survol() ? 'La souris est dessus' : 'Survolez-moi' }}
</div>
<p>Nom : {{ saisie() }} – ville : {{ ville() }} – niveau : {{ niveau() }}</p>
<p>Journal : {{ journal().join(' | ') }}</p>
`,
styles: `.survol { display: inline-block; padding: 6px 10px; border: 1px dashed #888; }`,
})
export class BasesEvenements {
protected readonly saisie = signal('');
protected readonly ville = signal('');
protected readonly niveau = signal('Débutant');
protected readonly survol = signal(false);
protected readonly journal = signal<string[]>([]);
// $event est un MouseEvent : position, nombre de clics, touches enfoncées…
protected onClic(e: MouseEvent): void {
this.noter(`clic n°${e.detail}${e.shiftKey ? ' + Maj' : ''}`);
}
// $event est un Event : on précise le type de la cible dans le code TypeScript
protected onSaisie(e: Event): void {
this.ville.set((e.target as HTMLInputElement).value);
}
protected noter(texte: string): void {
this.journal.update((j) => [...j, texte].slice(-5));
}
}
(Clic, puis Maj + Clic, puis double-clic)
Journal : clic n°1 | clic n°1 + Maj | clic n°1 | clic n°2 | double-clic
(saisie de « Amira » et « Sousse », choix « Avancé », survol)
Nom : Amira – ville : Sousse – niveau : Avancé La souris est dessus

- Un double-clic produit d'abord deux événements
click(avecdetail= 1 puis 2), puis ledblclick. Si un même élément écoute les deux, le clic simple sera aussi exécuté. - Trois façons de lire une valeur : la variable de référence (
champ.value), une méthode qui précise le type de la cible ((e.target as HTMLInputElement).value), ou$any($event.target).value, qui désactive la vérification de type et doit rester exceptionnel. (input)se déclenche à chaque frappe ;(change)seulement quand la valeur est validée (choix d'une liste, case cochée, sortie d'un champ modifié).mouseenteretmouseleavene remontent pas vers les parents, contrairement àmouseoveretmouseout: ils sont plus simples pour un effet de survol.- Attention : oublier les parenthèses de l'appel,
(click)="vider", n'exécute rien. Angular 22 le signale :NG8111: Function in event binding should be invoked: vider().
-
Les événements clavier et les filtres de touches
(keydown)se déclenche quand une touche est enfoncée (et se répète si on la maintient),(keyup)quand elle est relâchée.$eventest unKeyboardEvent:keydonne le caractère,codela position physique de la touche.- Angular ajoute les filtres de touches:
(keydown.enter)n'exécute l'instruction que pour la touche Entrée. On peut ajouter des modificateurs :control,shift,alt,meta.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-clavier',
template: `
<input
#champ
placeholder="Tâche puis Entrée"
(keydown)="derniere.set($event.key + ' / ' + $event.code)"
(keydown.enter)="ajouter(champ)"
(keydown.escape)="champ.value = ''"
(keydown.arrowup)="rappeler(champ)"
(keydown.control.s)="$event.preventDefault(); enregistrer()"
(keydown.control.code.keyz)="annuler()"
/>
<p>Dernière touche : {{ derniere() }}</p>
<p>Tâches : {{ taches().join(', ') }}</p>
<p>{{ message() }}</p>
`,
})
export class Clavier {
protected readonly derniere = signal('');
protected readonly taches = signal<string[]>(['Installer Node']);
protected readonly message = signal('');
protected ajouter(champ: HTMLInputElement): void {
const texte = champ.value.trim();
if (!texte) return;
this.taches.update((t) => [...t, texte]);
champ.value = '';
}
protected rappeler(champ: HTMLInputElement): void {
champ.value = this.taches().at(-1) ?? '';
}
protected annuler(): void {
this.taches.update((t) => t.slice(0, -1));
this.message.set('Dernière tâche annulée');
}
protected enregistrer(): void {
this.message.set(`${this.taches().length} tâche(s) enregistrée(s)`);
}
}
(« Créer le projet » + Entrée, puis « abc » + Échap)
Dernière touche : Escape / Escape
Tâches : Installer Node, Créer le projet (le champ est vidé)
(Flèche haut) le champ affiche « Créer le projet »
(Ctrl + S) 2 tâche(s) enregistrée(s) ← la page n'est pas enregistrée par le navigateur
(Ctrl + Z) Dernière tâche annulée — Tâches : Installer Node
(Ctrl + Entrée) rien : (keydown.enter) ne réagit pas avec Ctrl

- Plusieurs écouteurs peuvent cohabiter sur le même élément :
(keydown)s'exécute pour toutes les touches, et les filtres en plus pour leur touche. $event.preventDefault(); enregistrer(): on peut enchaîner plusieurs instructions avec un point-virgule. Ici,preventDefault()empêche le navigateur d'ouvrir sa fenêtre « Enregistrer la page ».(keydown.control.code.keyz)se base sur la position de la touche (event.code) : le raccourci reste au même endroit sur un clavier AZERTY ou QWERTY. Sanscode, le filtre suit la lettre produite.- Piège : un nom de touche mal écrit, comme
(keydown.entre), ne provoque aucune erreur : l'instruction n'est simplement jamais exécutée. Vérifiez l'orthographe :enter,escape,arrowup,space,tab,dot…
-
Propagation et comportement par défaut
- Un événement comme
clickest d'abord reçu par l'élément cliqué (la cible), puis il remonte vers chacun de ses ancêtres : c'est la propagation ou « bulle ». Tous les écouteurs rencontrés sont exécutés, du plus profond au plus haut. $event.stopPropagation()arrête la remontée.$event.preventDefault()annule l'action par défaut du navigateur (suivre un lien, envoyer un formulaire, afficher le menu du clic droit…). Les deux sont indépendants.
- Un événement comme
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-propagation',
template: `
<div class="ligne" (click)="noter('ligne', $event)">
Formation Angular
<span class="actions" (click)="noter('actions', $event)">
<button (click)="noter('détails', $event)">Détails</button>
<button (click)="supprimer($event)">Supprimer</button>
</span>
</div>
<a href="https://angular.dev" (click)="$event.preventDefault(); noter('lien bloqué', $event)">Documentation</a>
<form (submit)="$event.preventDefault(); noter('formulaire', $event)">
<button>Envoyer</button>
</form>
<p>Ordre : {{ journal().join(' → ') }}</p>
<button (click)="journal.set([])">Vider</button>
`,
styles: `.ligne { padding: 8px; background: #eef; } .actions { padding: 4px; background: #dde; }`,
})
export class Propagation {
protected readonly journal = signal<string[]>([]);
protected noter(qui: string, e: Event): void {
const cible = (e.target as HTMLElement).tagName.toLowerCase();
this.journal.update((j) => [...j, `${qui} (cible : ${cible})`]);
}
protected supprimer(e: MouseEvent): void {
e.stopPropagation(); // la ligne et les actions ne reçoivent pas ce clic
this.journal.update((j) => [...j, 'supprimer seulement']);
}
}
(clic sur Détails) Ordre : détails (cible : button) → actions (cible : button) → ligne (cible : button)
(clic sur Supprimer) Ordre : supprimer seulement
(clic sur la ligne) Ordre : ligne (cible : div)
(lien puis Envoyer) Ordre : lien bloqué (cible : a) → formulaire (cible : form) – la page ne change pas

$event.targetest l'élément d'origine (ici le bouton), même dans l'écouteur de la ligne. L'élément qui écoute est$event.currentTarget.- La propagation est utile : un seul écouteur sur une liste peut gérer les clics de toutes ses lignes. On ne l'arrête que lorsqu'une action ne doit concerner qu'un élément, comme le bouton Supprimer d'une ligne cliquable.
- Sans
preventDefault(), lesubmitd'un formulaire recharge la page et l'état de l'application est perdu. AvecFormsModule, utilisez plutôt(ngSubmit), qui s'en charge (voir « Le two-way data binding et ngModel »).
-
Écouter la fenêtre, le document et l'élément hôte
- Certains événements ne concernent aucun élément du template : le redimensionnement de la fenêtre, une touche pressée n'importe où, un clic en dehors d'un menu… On les écoute avec la propriété
hostdu décorateur, en préfixant l'événement parwindow:,document:oubody:. - Sans préfixe, l'écouteur est placé sur l'élément hôte, c'est-à-dire la balise du composant lui-même (
<app-globaux>). Les écouteurs sont retirés automatiquement à la destruction du composant.
- Certains événements ne concernent aucun élément du template : le redimensionnement de la fenêtre, une touche pressée n'importe où, un clic en dehors d'un menu… On les écoute avec la propriété
import { Component, ElementRef, inject, signal } from '@angular/core';
@Component({
selector: 'app-globaux',
host: {
'(window:resize)': 'redimensionner()',
'(document:keydown.escape)': 'ouvert.set(false)',
'(document:click)': 'clicDocument($event)',
},
template: `
<p>Largeur de la fenêtre : {{ largeur() }} px ({{ largeur() < 600 ? 'mobile' : 'bureau' }})</p>
<div class="menu">
<button (click)="ouvert.set(!ouvert())">Compte ▾</button>
@if (ouvert()) {
<ul><li>Profil</li><li>Paramètres</li><li>Déconnexion</li></ul>
}
</div>
<p>Menu : {{ ouvert() ? 'ouvert' : 'fermé' }}</p>
`,
styles: `.menu { position: relative; display: inline-block; } ul { position: absolute; margin: 0; padding: 6px 18px; background: #fff; border: 1px solid #ccc; }`,
})
export class Globaux {
private readonly hote = inject(ElementRef<HTMLElement>);
protected readonly largeur = signal(window.innerWidth);
protected readonly ouvert = signal(false);
// window n'est pas accessible dans le template : on passe par une méthode
protected redimensionner(): void {
this.largeur.set(window.innerWidth);
}
// Un clic n'importe où dans la page : on ferme le menu s'il a eu lieu hors du composant
protected clicDocument(e: MouseEvent): void {
if (!this.hote.nativeElement.contains(e.target as Node)) {
this.ouvert.set(false);
}
}
}
Largeur de la fenêtre : 900 px (bureau)
(fenêtre réduite à 500 px) Largeur de la fenêtre : 500 px (mobile)
(clic sur Compte ▾) Menu : ouvert
(clic sur « Profil ») Menu : ouvert ← clic à l'intérieur du composant
(clic ailleurs dans la page) Menu : fermé
(Compte ▾ puis Échap) Menu : fermé

- Le décorateur
@HostListener('window:resize'), placé au-dessus d'une méthode, fait la même chose. Il fonctionne toujours, mais la documentation d'Angular recommande la propriétéhost, qui regroupe tous les écouteurs et liaisons de l'hôte au même endroit. - Le « clic à l'extérieur » s'appuie sur la propagation : tout clic finit par atteindre le document.
contains(e.target)indique s'il a eu lieu dans le composant. - Attention : une instruction d'événement ne peut utiliser que les membres du composant.
'largeur.set(window.innerWidth)'est refusé :TS2339: Property 'window' does not exist on type 'Globaux'.Passez par une méthode.
-
Créer ses propres événements avec
output()- Un composant peut, lui aussi, déclencher des événements. On déclare une sortie avec
output<Type>()et on l'émet avecemit(valeur). Le parent l'écoute exactement comme un événement du navigateur :(inscrire)="inscrire($event)". - C'est le pendant de
input(): les données descendent du parent vers l'enfant par les entrées, les événements remontent de l'enfant vers le parent par les sorties. L'enfant ne sait pas ce que le parent fera de l'information : il reste réutilisable.
- Un composant peut, lui aussi, déclencher des événements. On déclare une sortie avec
import { Component, input, output, signal } from '@angular/core';
export interface Formation {
id: number;
titre: string;
places: number;
}
// Composant enfant : il annonce ce qui se passe, sans décider à la place du parent
@Component({
selector: 'app-fiche-formation',
template: `
<strong>{{ formation().titre }}</strong> – {{ formation().places }} place(s)
<button (click)="inscrire.emit(formation().id)" [disabled]="!formation().places">S'inscrire</button>
<button (click)="favori.emit()">★</button>
`,
})
export class FicheFormation {
readonly formation = input.required<Formation>();
readonly inscrire = output<number>(); // l'événement transporte un nombre
readonly favori = output<void>(); // un événement sans donnée
}
// Composant parent : il écoute les événements comme ceux du navigateur
@Component({
selector: 'app-sortie',
imports: [FicheFormation],
template: `
@for (f of formations(); track f.id) {
<p (click)="clics.update((n) => n + 1)">
<app-fiche-formation [formation]="f" (inscrire)="inscrire($event)" (favori)="basculerFavori(f.titre)" />
</p>
}
<p>{{ message() }}</p>
<p>Favoris : {{ favoris().join(', ') || 'aucun' }} – clics reçus par les paragraphes : {{ clics() }}</p>
`,
})
export class Sortie {
protected readonly formations = signal<Formation[]>([
{ id: 1, titre: 'Angular', places: 2 },
{ id: 2, titre: 'TypeScript', places: 1 },
]);
protected readonly message = signal('');
protected readonly clics = signal(0);
protected readonly favoris = signal<string[]>([]);
protected basculerFavori(titre: string): void {
this.favoris.update((f) => (f.includes(titre) ? f.filter((t) => t !== titre) : [...f, titre]));
}
protected inscrire(id: number): void {
this.formations.update((liste) => liste.map((f) => (f.id === id ? { ...f, places: f.places - 1 } : f)));
const f = this.formations().find((x) => x.id === id)!;
this.message.set(`Inscription à ${f.titre} : il reste ${f.places} place(s)`);
}
}
(S'inscrire à Angular) Inscription à Angular : il reste 1 place(s)
(S'inscrire à Angular) Inscription à Angular : il reste 0 place(s) ← bouton désactivé
(★ TypeScript puis ★ Angular) Favoris : TypeScript, Angular
(★ TypeScript à nouveau) Favoris : Angular – clics reçus par les paragraphes : 5

$eventa le type de la sortie : unnumberpourinscrire. Le compilateur vérifie les deux côtés :(inscrire)="noter($event.toUpperCase())"donneTS2339: Property 'toUpperCase' does not exist on type 'number'., etinscrire.emit('3')donneTS2345: Argument of type 'string' is not assignable to parameter of type 'number'.- Une sortie
output<void>()s'émet sans valeur :favori.emit(). Pour une sortie typée, oublier la valeur donneTS2554: Expected 1 arguments, but got 0. - Une sortie ne remonte pas: seul le parent direct peut l'écouter. En revanche, les clics du navigateur traversent les composants : le compteur des paragraphes augmente à chaque clic sur un bouton de l'enfant.
- Pour un échange dans les deux sens (une valeur que le parent fournit et que l'enfant modifie), utilisez
model()et[( )](voir « Le two-way data binding et ngModel »). La communication entre composants sera approfondie dans un prochain chapitre. - Piège : ne donnez pas à une sortie le nom d'un événement du navigateur. Avec
click = output<string>(), l'écouteur(click)du parent est exécuté deux fois par clic : une fois avec la chaîne émise, une fois avec lePointerEventdu navigateur qui remonte.
-
Écouter par programme
- Il est parfois nécessaire d'écouter un événement depuis le code TypeScript : écouteur ajouté sous condition, événement très fréquent à limiter, événement de la fenêtre traité par une bibliothèque RxJS…
Renderer2.listen(cible, evenement, fonction)renvoie une fonction qui retire l'écouteur ; on l'appelle dansDestroyRef.onDestroy().fromEvent()de RxJS transforme un événement en observable, quetoSignal()transforme en signal.
import { Component, DestroyRef, Renderer2, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { fromEvent, map, throttleTime } from 'rxjs';
@Component({
selector: 'app-programme',
template: `
<p>Connexion : {{ enLigne() ? 'en ligne' : 'hors ligne' }}</p>
<p>Copies (propriété ordinaire) : {{ copies }}</p>
<p>Souris : {{ souris().x }}, {{ souris().y }}</p>
`,
})
export class Programme {
protected readonly enLigne = signal(navigator.onLine);
protected copies = 0;
// fromEvent + throttleTime : au plus une position toutes les 100 ms
protected readonly souris = toSignal(
fromEvent<PointerEvent>(document, 'pointermove').pipe(
throttleTime(100),
map((e) => ({ x: Math.round(e.clientX), y: Math.round(e.clientY) })),
),
{ initialValue: { x: 0, y: 0 } },
);
constructor() {
const renderer = inject(Renderer2);
const arreter = [
renderer.listen('window', 'online', () => this.enLigne.set(true)),
renderer.listen('window', 'offline', () => this.enLigne.set(false)),
renderer.listen('document', 'copy', () => {
this.copies++; // modifiée, mais Angular n'est pas prévenu
console.log(`copies : ${this.copies}`);
}),
];
inject(DestroyRef).onDestroy(() => arreter.forEach((f) => f()));
}
}
(2 copies avec Ctrl + C) Copies (propriété ordinaire) : 0 console : copies : 1, copies : 2
(connexion coupée) Connexion : hors ligne
Copies (propriété ordinaire) : 2 ← affichée seulement maintenant
(souris déplacée) Souris : 281, 187 ← au plus 10 mises à jour par seconde
- Un écouteur placé dans le template prévient Angular. Un écouteur ajouté par programme ne le fait pas : la propriété
copieschange, mais l'affichage reste à 0 jusqu'à ce qu'un signal provoque une mise à jour. Dans un écouteur programmé, modifiez toujours un signal. throttleTime(100)ne garde qu'un événement toutes les 100 ms. Pourpointermove,scrollouwheel, qui se déclenchent des dizaines de fois par seconde, cette limitation évite des mises à jour inutiles.toSignal()se désabonne tout seul à la destruction du composant. AvecRenderer2, c'est à vous d'appeler les fonctions de retrait.
-
Les pièges à éviter
- Oublier les parenthèses d'appel:
(click)="vider"n'exécute rien (avertissementNG8111). - Écrire du code complexe dans le template: une instruction d'événement n'accepte ni
let, ni++, niif(erreurNG5002: Parser Error) et n'accède pas aux variables globales (window,document,Math). Au-delà d'un appel ou d'unset(), créez une méthode. - Mal écrire un filtre de touche:
(keydown.entre)est accepté sans erreur et ne réagit jamais. - Oublier
preventDefault()sur unsubmit(la page se recharge), un raccourci clavier déjà utilisé par le navigateur, uncontextmenupersonnalisé ou undragover(le dépôt est refusé). - Arrêter la propagation sans raison: les écouteurs du document (menus, fenêtres modales) ne voient plus le clic et ne se ferment pas.
- Nommer une sortie comme un événement DOM (
click,change,input…) : le parent reçoit les deux. - Modifier une propriété ordinaire dans un écouteur programmé: l'affichage n'est pas mis à jour ; utilisez un signal.
- Oublier les parenthèses d'appel:

-
Aide-mémoire

-
Activité 1 : une calculatrice au clavier et à la souris
- Générer le composant
act-evenements/calculatrice: un écran (expression et résultat) et 20 touches (C,⌫,%, les quatre opérations, les chiffres,00, la virgule et=). Chaque touche appelleappuyer(touche)au(click).- Le clavier physique doit aussi fonctionner : écouter
'(document:keydown)'danshostet traduire chaque touche (*→ ×,/→ ÷, Entrée → =, Retour arrière → ⌫, Échap → C). - Appeler
preventDefault()uniquement pour les touches utilisées : la touche/ouvre la recherche rapide de certains navigateurs. - La touche appuyée s'entoure de jaune pendant 150 ms, qu'elle vienne de la souris ou du clavier.
- Le résultat est un
computed. Le calcul respecte la priorité de × et ÷, sans utilisereval(); une division par zéro affiche « Erreur ». - Deux opérateurs de suite : le second remplace le premier (7 − × 2 donne 7 × 2). Après
=, un opérateur continue le calcul et un chiffre en commence un nouveau.
- Le clavier physique doit aussi fonctionner : écouter
- Générer le composant

- Taper au clavier
12+3*4puis Entrée : vérifier 24. Continuer avec/4puis Entrée : vérifier 6. Tester250*12%(30) et1/0. - Pourquoi ne pas écouter
(keydown)sur un des boutons plutôt que sur le document ?
-
Activité 2 : un tableau Kanban en glisser-déposer
- Générer le composant
act-evenements/kanbanavec trois colonnes (À faire, En cours, Terminé) et un signaltaches({ id, titre, colonne }). Utiliser le glisser-déposer natif du navigateur :- chaque tâche a l'attribut
draggable="true"; au(dragstart), enregistrer son identifiant avec$event.dataTransfer.setData()et l'estomper ; - chaque colonne écoute
(dragover)avecpreventDefault()(sinon le dépôt est refusé), se met en surbrillance au(dragenter)et la perd au(dragleave); - au
(drop), lire l'identifiant, changer la colonne de la tâche avecupdate()et afficher « « titre » déplacée vers … » ; - afficher le nombre de tâches de chaque colonne avec un
computed.
- chaque tâche a l'attribut
- Générer le composant

- Supprimer
(dragover)="$event.preventDefault()": peut-on encore déposer une tâche ? Pourquoi ? dragleavese déclenche aussi quand le pointeur passe sur une tâche à l'intérieur de la colonne. Expliquer comment la méthodequitter()évite le clignotement de la surbrillance.
-
Activité 3 : un menu contextuel personnalisé
- Générer le composant
act-evenements/menu-contextuelqui affiche une liste de fichiers. Un clic droit sur un fichier ouvre un menu personnalisé à la position de la souris :(contextmenu)avecpreventDefault()pour masquer le menu du navigateur ; la position vient declientX/clientYet le fichier concerné est surligné ;- le menu propose Renommer, Dupliquer et Supprimer; un double-clic sur un fichier le renomme aussi ;
- le menu se ferme au clic ailleurs dans la page, à la touche Échap et au redimensionnement de la fenêtre (trois écouteurs dans
host) ; - un clic dans le menu ne doit pas le fermer avant que l'action soit exécutée : utiliser
stopPropagation().
- Générer le composant

- Pourquoi le clic droit qui ouvre le menu ne le referme-t-il pas aussitôt ? Quel événement le document reçoit-il ?
- Supprimer
(click)="$event.stopPropagation()"sur le menu : les actions fonctionnent-elles encore ? Tester, puis expliquer l'ordre d'exécution des écouteurs.
-
Activité 4 : un composant de notation avec
output()- Créer un composant réutilisable
app-etoilesqui affiche cinq étoiles :- une entrée
note = input(0)et une sortienoter = output<number>(), émise au clic sur une étoile, seulement si la note change ; - un aperçu au survol (
(mouseenter)sur chaque étoile,'(mouseleave)'sur l'hôte), calculé avec uncomputed; - l'accès au clavier : l'hôte reçoit
tabindex="0", le rôleslideretaria-valuenow; les flèches ← et → changent la note de 1 (entre 1 et 5).
- une entrée
- Le composant parent
act-evenements/notationaffiche trois formations avec leurs avis. Il écoute(noter), enregistre la note de l'utilisateur et met la moyenne à jour.
- Créer un composant réutilisable

- Noter Angular 5 / 5 : vérifier que la moyenne passe de 4,3 à 4,5 avec « (3 avis) ». Cliquer à nouveau sur la 5e étoile : un événement est-il émis ?
- Pourquoi l'enfant émet-il la note au lieu de modifier lui-même la liste des avis ?
-
Activité 5 : une ardoise de dessin
- Générer le composant
act-evenements/ardoise: un<canvas>sur lequel on dessine à la souris, au doigt ou au stylet grâce aux événements de pointeur.(pointerdown)commence un trait (couleur, épaisseur, liste de points) et appellesetPointerCapture();(pointermove)ajoute des points ;(pointerup)et(pointercancel)terminent le trait ;- une palette de cinq couleurs et un curseur d'épaisseur (2 à 16) ;
- les traits terminés sont stockés dans un signal ;
afterRenderEffect()redessine le canvas après chaque changement ; - Annuler et le raccourci Ctrl + Z (
'(document:keydown.control.z)') retirent le dernier trait ; Effacer vide l'ardoise ; un texte indique le nombre de traits et de points.
- Générer le composant

- Commencer un trait dans le canvas et relâcher le bouton en dehors : le trait est-il terminé ? Recommencer sans
setPointerCapture()et comparer. - Pourquoi le style
touch-action: noneest-il indispensable sur un écran tactile ? - Faire un commit après chaque activité, puis un push :
git commit -m "Événements : activité N".
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG8111: Function in event binding should be invoked: vider(). (avertissement) |
Méthode sans parenthèses : (click)="vider" |
(click)="vider()" |
TS2531: Object is possibly 'null'. et TS2339: Property 'value' does not exist on type 'EventTarget'. |
$event.target.value sur un <select> ou un <textarea> |
Variable #ref, ou méthode avec (e.target as HTMLSelectElement).value |
TS2339: Property 'window' does not exist on type 'Globaux'. |
Variable globale utilisée dans une instruction du template ou de host |
Passer par une méthode du composant |
NG5002: Parser Error: Unexpected token let … ou Unexpected end of expression: n++ |
let, ++ ou autre code non autorisé dans le template |
n.update(v => v + 1), ou une méthode |
TS2339: Property 'toUpperCase' does not exist on type 'number'. |
$event d'une sortie utilisé avec le mauvais type |
Respecter le type de output<T>() |
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. |
emit() avec une valeur du mauvais type |
Convertir la valeur, ou changer le type de la sortie |
TS2554: Expected 1 arguments, but got 0. |
emit() sans valeur sur une sortie typée |
Passer la valeur, ou déclarer output<void>() |
| Le raccourci ne réagit jamais, sans erreur | Nom de touche mal écrit (keydown.entre) ou modificateur en trop |
enter, escape, arrowup… ; un filtre est exact |
| La page se recharge à l'envoi du formulaire | (submit) sans preventDefault() |
$event.preventDefault() ou (ngSubmit) |
| Le dépôt (drop) n'est jamais déclenché | (dragover) sans preventDefault() |
(dragover)="$event.preventDefault()" |
| Le parent reçoit deux fois l'événement de l'enfant | Sortie nommée comme un événement DOM (click) |
Renommer la sortie (choisir, valider…) |
| Un menu ne se ferme plus au clic à l'extérieur | stopPropagation() qui empêche le clic d'atteindre le document |
Ne l'arrêter que lorsque c'est nécessaire |
| L'affichage ne suit pas un écouteur ajouté par programme | Propriété ordinaire modifiée hors du template | Modifier un signal |
-
À retenir
(evenement)="instruction"écoute un événement ;$eventcontient l'objet du navigateur (MouseEvent,KeyboardEvent…) ou la valeur émise par une sortie.- Les filtres de touches (
(keydown.enter),(keydown.control.s)) simplifient le clavier ; ils sont exacts, et une faute de frappe passe inaperçue. - Un événement remonte vers les ancêtres :
stopPropagation()arrête la remontée,preventDefault()annule l'action par défaut. - La propriété
hostécoute l'élément hôte,window:,document:etbody:. output<T>()etemit()créent des événements typés, reçus par le parent direct.- Les instructions d'événement restent courtes : la logique va dans des méthodes, les données qui changent dans des signals.
Le template écoute, la méthode décide, le signal retient : avec cette répartition, chaque interaction de l'utilisateur reste simple à lire, à tester et à faire évoluer.