Retour au cours

frontend / javascript

Événements DOM

Leçon 91 exercice

Explication

Ce que vous allez apprendre

  • Attacher un gestionnaire d'événement avec addEventListener
  • Expliquer les phases de capture et de bubbling d'un événement
  • Mettre en place la délégation d'événements avec event.target
  • Choisir entre preventDefault() et stopPropagation() selon le besoin
  • Diagnostiquer pourquoi un événement attaché à un élément ajouté dynamiquement ne se déclenche pas

Dans quel contexte ?

Un développeur ajoute une liste de tâches où chaque ligne a un bouton "Supprimer" généré dynamiquement en JavaScript. Il attache addEventListener sur chaque bouton au moment de sa création, mais les tâches ajoutées après un rechargement AJAX ne réagissent plus au clic — leurs listeners n'ont jamais été attachés. La solution standard : déléguer l'écoute au conteneur parent (liste.addEventListener("click", ...)) et identifier le bouton cliqué via event.target.closest(".btn-supprimer"), ce qui fonctionne pour tous les éléments, présents ou futurs.

1. Être notifié quand il se passe quelque chose

Une page web statique ne sert à rien sans interactivité. Le système d'événements du navigateur notifie votre code chaque fois qu'il se passe quelque chose — un clic, une saisie clavier, une soumission de formulaire. addEventListener est la porte d'entrée moderne vers ce mécanisme.

2. Un clic traverse trois phases

Quand vous cliquez sur un élément imbriqué dans plusieurs autres, l'événement traverse trois étapes : il descend d'abord depuis la racine du document jusqu'à l'élément cliqué (capture), l'atteint (la cible), puis remonte vers la racine en notifiant chaque ancêtre au passage (le "bubbling", actif par défaut).

PhaseDirectionActivée par défaut ?
CaptureRacine → élément cibleNon (nécessite { capture: true })
CibleSur l'élément cliquéToujours
BubblingÉlément cible → racineOui

Prérequis

Cette leçon suppose une bonne maîtrise de la sélection et de la manipulation du DOM (leçon précédente) : vous devez être à l'aise avec querySelector avant d'attacher des événements dessus.

3. Ce que cette remontée rend possible

Cette remontée n'est pas juste un détail interne : elle rend possible la "délégation d'événements". Plutôt que d'attacher un listener à chacun des cent éléments d'une liste, on en attache un seul sur leur parent commun, et on identifie l'élément réellement cliqué via event.target.

4. Pourquoi cette technique est si utile

Cette délégation est plus performante qu'attacher cent listeners séparés, et elle fonctionne même pour des éléments ajoutés dynamiquement après coup — sans code supplémentaire pour les "rebrancher".

Astuce

event.target est l'élément précis qui a déclenché l'événement (peut être un enfant profond), tandis que event.currentTarget est l'élément sur lequel le listener est attaché. Utilisez event.target.closest(".ma-classe") pour retrouver l'élément pertinent en délégation.

5. Deux méthodes souvent confondues

preventDefault() bloque le comportement natif du navigateur (suivre un lien, recharger la page sur une soumission de formulaire). stopPropagation() empêche l'événement de continuer sa remontée vers les ancêtres — deux besoins différents, à ne pas mélanger.

Cette leçon prépare directement la suite du cours : la plupart des interactions réelles d'une application (chargement de données au clic, recherche en temps réel) combinent événements DOM et code asynchrone, sujet des prochains chapitres.

Commandes & code

Événements DOM

Réagir aux interactions utilisateur et comprendre la propagation.

js
const bouton = document.querySelector("#mon-bouton");

// addEventListener : methode moderne et recommandee (multiple listeners possibles)
bouton.addEventListener("click", (event) => {
    console.log("Bouton clique", event.target);
});

// Fonction nommee pour pouvoir retirer le listener plus tard
function gererClic(event) {
    console.log("Clic gere");
}
bouton.addEventListener("click", gererClic);
bouton.removeEventListener("click", gererClic);        // necessite la MEME reference de fonction

// L'objet Event : proprietes utiles
bouton.addEventListener("click", (event) => {
    console.log(event.type);          // "click"
    console.log(event.target);          // element qui a declenche l'evenement
    console.log(event.currentTarget);     // element sur lequel le listener est attache
    console.log(event.timeStamp);
});

// preventDefault : empecher le comportement natif du navigateur
const lien = document.querySelector("a");
lien.addEventListener("click", (event) => {
    event.preventDefault();          // empeche la navigation
    console.log("Navigation bloquee");
});

const formulaire = document.querySelector("form");
formulaire.addEventListener("submit", (event) => {
    event.preventDefault();          // empeche le rechargement de page
    // validation + envoi via fetch a la place
});

// --- Propagation : capture, target, bubbling ---
// capture (descend) -> target -> bubbling (remonte) : 3 phases
document.querySelector("#parent").addEventListener(
    "click",
    () => console.log("Phase de capture (parent)"),
    { capture: true }                 // s'execute EN DESCENDANT, avant l'enfant
);
document.querySelector("#enfant").addEventListener("click", () => {
    console.log("Cible atteinte (enfant)");
});
document.querySelector("#parent").addEventListener("click", () => {
    console.log("Phase de bubbling (parent)");     // s'execute APRES l'enfant, en remontant
});

// stopPropagation : empeche la remontee (bubbling) vers les ancetres
document.querySelector("#enfant").addEventListener("click", (event) => {
    event.stopPropagation();
});

// --- Delegation d'evenements : un seul listener pour de nombreux enfants (performant) ---
document.querySelector("#liste").addEventListener("click", (event) => {
    if (event.target.matches("li.item")) {
        console.log("Item clique :", event.target.textContent);
    }
    // fonctionne meme pour des <li> ajoutes DYNAMIQUEMENT apres coup -- pas de re-binding necessaire
});

// --- Options du listener ---
bouton.addEventListener(
    "click",
    () => console.log("Une seule fois"),
    { once: true }                       // se retire automatiquement apres le premier appel
);

window.addEventListener(
    "scroll",
    () => console.log("scroll"),
    { passive: true }                     // indique qu'on n'appellera JAMAIS preventDefault : optimise le scroll
);

// --- Debounce/throttle appliques aux evenements haute frequence (voir lecon dediee) ---
let timeoutId;
window.addEventListener("resize", () => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => console.log("Resize termine"), 200);
});

// --- CustomEvent : creer et dispatcher ses propres evenements ---
const evenementPerso = new CustomEvent("produit-ajoute", {
    detail: { id: 42, nom: "Clavier" },
    bubbles: true,
});
document.querySelector("#panier").addEventListener("produit-ajoute", (event) => {
    console.log("Produit recu :", event.detail);
});
document.querySelector("#panier").dispatchEvent(evenementPerso);

// --- AbortController : nettoyer plusieurs listeners d'un coup ---
const controller = new AbortController();
document.addEventListener("click", () => console.log("clic"), { signal: controller.signal });
document.addEventListener("keydown", () => console.log("touche"), { signal: controller.signal });
controller.abort();     // retire TOUS les listeners lies a ce signal en une fois

Résumé

  • Les événements traversent 3 phases : capture (descend), target, bubbling (remonte).
  • La délégation d'événements (un listener sur le parent) est plus performante pour de nombreux enfants dynamiques.
  • { once: true }, { passive: true } et AbortController simplifient la gestion du cycle de vie des listeners.
  • CustomEvent permet de créer un système d'événements applicatif propre, découplé.

Exercices pratiques

1 disponible
1

Mission : la liste de tâches qui n'écoute pas les nouveaux éléments

Objectif : Remplacer une attache de listeners un par un par une délégation d'événements, et corriger un stopPropagation mal placé.

Contexte

Sur l'application de tâches de Technologik, chaque <li class="tache"> reçoit un listener de clic individuel pour la marquer comme terminée. Problème : les tâches ajoutées dynamiquement après le chargement de la page (via un bouton "Ajouter") ne réagissent jamais au clic. Le code actuel :

js
document.querySelectorAll("li.tache").forEach((li) => {
  li.addEventListener("click", () => li.classList.toggle("terminee"));
});

Un second bug s'ajoute : un bouton "Supprimer" à l'intérieur de chaque tâche appelle event.stopPropagation(), mais un menu contextuel censé s'ouvrir sur le clic du conteneur parent ne s'ouvre plus jamais, même sur une zone vide de la liste.

Résoudre l’exercice →