Apprenez en lisant dans l'ordre

Les événements — addEventListener et la propagation

Réagir aux clics avec addEventListener et event.target, les déléguer au parent grâce au bubbling, et utiliser stopPropagation, once et removeEventListener.

Une affectation à textContent modifie l'élément une seule fois, au moment où la ligne s'exécute. Pour augmenter un compteur à chaque clic sur un bouton, des lignes exécutées de haut en bas ne suffisent pas : rien dans le code ne dit quand le clic aura lieu.

Cet article présente addEventListener, qui appelle une fonction quand on clique, et la façon de gérer les clics sur des éléments enfants depuis leur parent.

Appeler une fonction au clic — addEventListener

Sur une fiche produit, tu veux qu'à chaque clic sur le bouton « Ajouter au panier », le nombre d'articles du panier augmente de 1 et s'affiche. Si tu écris simplement la ligne qui modifie ce nombre, elle s'exécute une seule fois, au chargement de la page, et les clics suivants sur le bouton ne changent plus rien.

Pour qu'une fonction soit appelée quand un événement se produit sur la page (un clic, une saisie…), on l'enregistre en passant le nom de l'événement et la fonction, comme dans addEventListener("click", fonction). La fonction enregistrée s'appelle un écouteur d'événement (event listener, ou simplement écouteur). Dans la console, tu ne peux pas cliquer avec la souris : tu déclenches donc les clics avec la méthode click, comme dans cartButton.click().

document.body.innerHTML = `<button id="add-to-cart">Ajouter au panier</button><p>Articles dans le panier : <span id="cart-count">0</span></p>`;
const cartButton = document.querySelector("#add-to-cart");
const cartCount = document.querySelector("#cart-count");

// La fonction à appeler au clic (ici, on se contente de la définir)
function addToCart() {
  cartCount.textContent = Number(cartCount.textContent) + 1;
}

// On passe la fonction elle-même pour l'enregistrer (addToCart n'est pas appelée sur cette ligne)
cartButton.addEventListener("click", addToCart);
console.log(cartCount.textContent);   // 0

// À chaque clic déclenché, la fonction enregistrée addToCart est appelée
cartButton.click();
cartButton.click();
console.log(cartCount.textContent);   // 2
Les lignes où addToCart s'exécute vraiment
addEventListener("click", addToCart)cartButton.click()(1er clic)cartButton.click()(2e clic)Pas d'appel :toujours 0addToCart appeléele nombre vaut 1Rappelée :le nombre vaut 2
La ligne d'enregistrement n'appelle pas addToCart : le nombre reste à 0. Le corps de la fonction s'exécute sur les lignes où un clic a lieu.

Sur une vraie page, tu n'écris pas click() : le navigateur appelle addToCart dès que l'utilisateur clique sur le bouton. Seuls les clics qui ont lieu après l'enregistrement déclenchent l'appel ; ceux qui précèdent la ligne d'enregistrement ne sont pas comptés.

Avec (), la fonction s'exécute une fois, à l'enregistrement

Si tu écris addEventListener("click", addToCart()), addToCart est appelée une fois sur la ligne d'enregistrement, et c'est sa valeur de retour, undefined, qui est passée. Aucune erreur ne s'affiche : difficile, donc, de comprendre pourquoi le nombre n'augmente pas au clic. Pour enregistrer une fonction, passe seulement son nom, sans ().

Sur une page d'auteur, change l'affichage du bouton d'abonnement à chaque clic. Le bouton followButton est déjà déclaré.

① Enregistre une fonction qui, à chaque clic, bascule la classe active et alterne le texte entre « Suivre » et « Abonné ».

② Déclenche un clic, puis affiche le HTML du bouton, balise comprise.

③ Déclenche un deuxième clic, puis affiche le HTML du bouton.

(Si tout s'exécute correctement, une explication apparaîtra.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Gérer le clic d'un enfant depuis son parent — bubbling et délégation

Dans une liste de commandes, tu veux maintenant lire le numéro de la commande sur laquelle on clique. Si tu récupères les lignes avec querySelectorAll et que tu enregistres un écouteur sur chacune avec forEach, seules les lignes présentes à ce moment-là en reçoivent un : un clic sur une ligne ajoutée plus tard avec append ne fait rien.

Un événement survenu sur un élément remonte ensuite vers son parent, puis vers le parent de celui-ci, et ainsi de suite : c'est le bubbling (ou « bouillonnement »). Un écouteur enregistré sur le ul parent est donc aussi appelé quand on clique sur un li. Le navigateur passe à l'écouteur un objet événement, qui contient les informations sur l'événement. Dans le code ci-dessous, cet argument s'appelle event, et sa propriété target donne l'élément sur lequel le clic a eu lieu à l'origine.

document.body.innerHTML = `<section id="order-panel"><ul id="order-list"><li data-id="A-101">Commande A-101</li><li data-id="A-102">Commande A-102</li></ul></section>`;
const orderList = document.querySelector("#order-list");
const orderPanel = document.querySelector("#order-panel");

// On enregistre sur le ul parent et sur la section qui l'entoure, pas sur les li
orderList.addEventListener("click", (event) => {
  console.log(`Reçu par le ul : ${event.target.dataset.id}`);
});
orderPanel.addEventListener("click", (event) => {
  console.log(`Reçu par la section : ${event.target.dataset.id}`);
});

// On déclenche un clic sur le li A-102
document.querySelector('[data-id="A-102"]').click();
// Reçu par le ul : A-102
// Reçu par la section : A-102
Le chemin du clic sur A-102
Élément section — son écouteur est appelé en 2e
  • event.target.dataset.id vaut A-102
Élément ul — son écouteur est appelé en 1er
  • event.target.dataset.id vaut A-102
li A-101 — pas cliqué
  • Le clic n'atteint pas les li voisins
li A-102 — celui sur lequel click() est appelé
  • event.target est ce li
  • Le clic commence à remonter à partir d'ici
Le clic passe du li au ul, puis à la section, sans jamais traverser le li voisin. event.target est le li A-102 partout.

Enregistrer un seul écouteur sur le parent et identifier l'enfant cliqué grâce à event.target, c'est ce qu'on appelle la délégation d'événements. Le clic sur un li est traité une fois arrivé au ul : le même écouteur est donc appelé pour un li ajouté avec append après l'enregistrement. event.target est l'élément le plus profond sur lequel on a cliqué : si le li contient un span et qu'on clique dessus, c'est le span qu'il désigne.

Sur l'écran de réservation d'une salle de réunion, récupère le créneau cliqué dans la liste des horaires. L'élément ul de la liste des créneaux, slotList, est déjà déclaré.

① Enregistre un seul écouteur sur la liste, qui affiche l'heure du créneau cliqué et lui ajoute la classe selected.

② Déclenche un clic sur le créneau de 10:00.

③ Crée un créneau de 16:00, ajoute-le à la fin de la liste et déclenche un clic dessus.

④ Affiche le HTML de la liste, balise ul comprise.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Empêcher le clic d'atteindre le parent — stopPropagation

Dans une liste de commandes, un clic sur une ligne ouvre le détail de la commande, et chaque ligne contient un bouton « Supprimer ». Le bouton est un enfant de la ligne : un clic sur « Supprimer » remonte donc aussi jusqu'à la ligne, et le détail de la commande s'ouvre alors même qu'on est en train de la supprimer.

Cette transmission de l'événement aux éléments parents s'appelle la propagation. Pour l'arrêter, appelle stopPropagation dans l'écouteur du bouton, sous la forme event.stopPropagation() : l'événement ne va pas plus loin que l'élément dont l'écouteur est en cours d'exécution. Les écouteurs des éléments parents, comme la ligne ou la liste, ne sont alors plus appelés.

document.body.innerHTML = `<ul><li id="order-row">Commande A-201 <button id="favorite">Favori</button><button id="delete">Supprimer</button></li></ul>`;
const orderRow = document.querySelector("#order-row");
const favoriteButton = document.querySelector("#favorite");
const deleteButton = document.querySelector("#delete");

// Un clic sur la ligne ouvre le détail
orderRow.addEventListener("click", () => console.log("Ouvrir le détail"));

// Favori : ouvrir le détail ne pose pas de problème, on laisse le clic remonter à la ligne
favoriteButton.addEventListener("click", () => console.log("Ajouté aux favoris"));
favoriteButton.click();   // Ajouté aux favoris, Ouvrir le détail (2 lignes)

// Supprimer : on appelle stopPropagation() pour que le clic n'atteigne pas la ligne
deleteButton.addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("Commande supprimée");
});
deleteButton.click();     // Commande supprimée
Un clic qui remonte, un clic arrêté
favoriteButton.click()Affiche « Ajoutéaux favoris »Non arrêté : ilatteint la ligneOuvrir le détails'affiche aussideleteButton.click()stopPropagation(),puis suppressionN'atteint pasla ligneOuvrir le détailne s'affiche pas
Le clic sur Favori n'est pas arrêté, puisque ouvrir le détail ne gêne pas : il atteint la ligne. Dès que tu appelles stopPropagation(), l'écouteur de la ligne n'est plus appelé.

Dans l'écouteur de suppression, « Commande supprimée » s'affiche bien, même si le console.log vient après stopPropagation(). Seule la remontée de l'événement vers les parents s'arrête ; la suite de l'écouteur qui a fait l'appel s'exécute normalement.

Un clic arrêté n'atteint pas non plus l'écouteur délégué du ul

stopPropagation() empêche l'événement d'atteindre non seulement la ligne qui contient directement le bouton, mais aussi tous les éléments situés plus haut. Si le ul de la liste porte un écouteur délégué qui journalise les actions de l'utilisateur, les clics sur le bouton de suppression n'y arriveront jamais. N'arrête que les clics dont aucun parent n'a besoin.

Dans la liste des notifications d'une messagerie d'équipe, un clic sur une ligne ouvre la notification, alors que le bouton sourdine de la ligne se contente de la mettre en sourdine. La ligne noticeRow est déjà déclarée, avec son écouteur.

① Enregistre sur le bouton sourdine un écouteur qui affiche « Mis en sourdine » et empêche le clic d'atteindre la ligne.

② Déclenche un clic sur le bouton sourdine.

③ Affiche le HTML de la ligne, balise li comprise.

④ Déclenche un clic sur la ligne elle-même.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Retirer un écouteur — once et removeEventListener

Si un utilisateur clique deux fois de suite sur le bouton « Valider la commande », l'écouteur enregistré est appelé deux fois, et la même commande part deux fois. Une fois le premier envoi pris en compte, il faut que l'écouteur ne soit plus appelé quand on clique de nouveau sur ce bouton.

Si tu passes { once: true } en troisième argument de addEventListener, l'écouteur est retiré automatiquement après son premier appel. Pour retirer un écouteur au moment de ton choix, appelle removeEventListener avec le nom de l'événement et la fonction : parmi les écouteurs enregistrés, il retire celui qui est strictement égal (===) à cette fonction.

document.body.innerHTML = `<button id="confirm">Valider la commande</button>`;
const confirmButton = document.querySelector("#confirm");
const sendOrder = () => console.log("Commande envoyée");

// { once: true } — retiré automatiquement après un premier appel
confirmButton.addEventListener("click", sendOrder, { once: true });
confirmButton.click();   // Commande envoyée
confirmButton.click();   // (rien ne s'affiche)

// Passer le sendOrder enregistré retire cet écouteur
confirmButton.addEventListener("click", sendOrder);
confirmButton.removeEventListener("click", sendOrder);
confirmButton.click();   // (rien ne s'affiche)

// Une fonction fléchée réécrite est une autre fonction, même avec le même corps : rien n'est retiré
confirmButton.addEventListener("click", () => console.log("Commande envoyée"));
confirmButton.removeEventListener("click", () => console.log("Commande envoyée"));
confirmButton.click();   // Commande envoyée
L'écouteur est-il appelé au clic suivant ?
confirmButton :3 façons deretirerEnregistré avec{ once: true }removeEventListener("click", sendOrder)removeEventListener("click", () => …)Retiré tout seulaprès un appelMême fonction :il est retiré=== vaut false :pas retiréPas appelé auclic suivantPas appelé auclic suivantAu clic suivant,affiche encoreCommande envoyée
once, ou la même fonction que celle enregistrée, retire l'écouteur. Passer une fonction fléchée réécrite ne le retire pas.

() => … crée une nouvelle fonction chaque fois que tu l'écris : même avec le même corps, la comparer avec === à la fonction enregistrée donne false. Un écouteur que tu retireras plus tard doit d'abord être rangé dans une variable, comme sendOrder, puis enregistré. Le tableau ci-dessous récapitule les techniques vues dans cet article.

ÉcritureCe que ça faitPoint d'attention
addEventListener("click", fn)Appelle fn à chaque clicPasser fn sans ()
event.targetL'élément cliquéToujours le plus profond
Délégation au parentReçoit les clics des enfantsInclut les enfants ajoutés
event.stopPropagation()Arrête la remontéeBloque aussi la délégation
{ once: true }Retire après un appel3e argument d'addEventListener
removeEventListener("click", fn)Retire l'écouteurPasser la même fonction

Voici l'écran d'inscription à un webinaire. registerButton, waitButton, attendeeCount et waitingCount sont déjà déclarés.

① Enregistre sur le bouton d'inscription un écouteur, appelé une seule fois, qui ajoute 1 au nombre d'inscrits.

② Déclenche deux clics sur le bouton d'inscription, puis affiche le nombre.

③ Enregistre sur le bouton de liste d'attente une fonction qui ajoute 1 au nombre de personnes en attente, puis déclenche un clic.

④ Retire la fonction de ③, déclenche un nouveau clic et affiche le nombre.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Avec saveButton.addEventListener("click", save());, quand save est-elle appelée ?

Question 2Dans un écouteur enregistré sur un ul, que vaut event.target quand tu appelles click() sur un li de ce ul ?

Question 3Après avoir enregistré () => send(), que se passe-t-il si tu appelles removeEventListener("click", () => send()) ?