Question 1Avec saveButton.addEventListener("click", save());, quand save est-elle appelée ?
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
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 ().
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
event.target.dataset.idvaut A-102
event.target.dataset.idvaut A-102
- Le clic n'atteint pas les li voisins
event.targetest ce li- Le clic commence à remonter à partir d'ici
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.
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
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.
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
() => … 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.
| Écriture | Ce que ça fait | Point d'attention |
|---|---|---|
| addEventListener("click", fn) | Appelle fn à chaque clic | Passer fn sans () |
| event.target | L'élément cliqué | Toujours le plus profond |
| Délégation au parent | Reçoit les clics des enfants | Inclut les enfants ajoutés |
| event.stopPropagation() | Arrête la remontée | Bloque aussi la délégation |
| { once: true } | Retire après un appel | 3e argument d'addEventListener |
| removeEventListener("click", fn) | Retire l'écouteur | Passer la même fonction |
Vérification des connaissances
Répondez à chaque question une par une.
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()) ?