Gérer les actions avec la délégation d’événements
Objectif : Traiter les clics sur les commandes des tâches avec un seul écouteur placé sur leur conteneur.
Une tâche contient plusieurs commandes, et la liste peut changer à mesure que vous en ajoutez ou en retirez. Placer un écouteur sur chaque commande complique le suivi des éléments créés au fil du temps. Vous allez centraliser le traitement des clics sur la liste et retrouver la commande concernée à partir de l’élément cliqué.
Pourquoi déléguer le clic ?
Quand vous cliquez sur un élément, le navigateur propage l’événement depuis sa cible vers ses éléments parents. Ainsi, un clic sur le bouton « Supprimer » passe aussi par la ligne de tâche et par la liste qui la contient. En plaçant un seul écouteur sur <ul data-liste>, vous pouvez traiter les clics des commandes présentes maintenant comme de celles ajoutées ensuite.
Le modèle fourni définit déjà data-action sur les commandes et data-id sur la ligne. Ces attributs data-* permettent de marquer un élément avec une information que JavaScript peut lire, sans ajouter de classe ni modifier la structure imposée. Le gestionnaire s’appuie sur ces repères pour savoir quoi faire et sur quelle tâche agir.
Identifier la commande et sa tâche
La cible de l’événement, accessible par event.target, peut être un élément enfant de la commande plutôt que le bouton lui-même. closest() remonte alors depuis cette cible jusqu’au bouton le plus proche qui porte data-action. Vous recherchez ensuite la ligne portant data-id afin d’obtenir l’identifiant de la tâche.
Le fichier ci-dessous reprend l’ajout temporaire de la leçon sur la soumission du formulaire. Les commandes des tâches sont traitées par délégation : basculer une case met à jour son état visuel, et supprimer une tâche retire sa ligne. Cette manipulation touche encore directement l’affichage ; une leçon ultérieure introduira l’état applicatif unique qui servira de source au rendu.
const formulaire = document.querySelector("[data-formulaire]");
const champ = document.querySelector("#nouvelle-tache");
const liste = document.querySelector("[data-liste]");
const modele = document.querySelector("[data-modele-tache]");
if (!formulaire || !champ || !liste || !modele) {
throw new Error("Un élément nécessaire au gestionnaire est absent de la page.");
}
function creerElementTache(titre) {
const fragment = modele.content.cloneNode(true);
const ligne = fragment.querySelector(".atelier-tache");
const titreElement = fragment.querySelector("[data-titre]");
const caseTache = fragment.querySelector('[data-action="basculer"]');
if (!ligne || !titreElement || !caseTache) {
throw new Error("Le modèle de tâche est incomplet.");
}
ligne.dataset.id = String(Date.now()) + String(Math.random()).slice(2);
titreElement.textContent = titre;
caseTache.checked = false;
return fragment;
}
function traiterClicTache(event) {
if (!(event.target instanceof Element)) {
return;
}
const commande = event.target.closest("[data-action]");
if (!commande || !liste.contains(commande)) {
return;
}
const ligne = commande.closest("[data-id]");
if (!ligne || !liste.contains(ligne)) {
return;
}
const identifiant = ligne.dataset.id;
if (commande.dataset.action === "basculer") {
const caseTache = commande;
if (caseTache instanceof HTMLInputElement) {
caseTache.setAttribute("aria-label", "Tâche terminée : " + identifiant);
}
return;
}
if (commande.dataset.action === "supprimer") {
ligne.remove();
}
}
function traiterSoumission(event) {
event.preventDefault();
const titre = new FormData(formulaire).get("titre");
if (typeof titre !== "string") {
return;
}
const titreNettoye = titre.trim();
if (titreNettoye.length === 0) {
champ.value = "";
return;
}
liste.append(creerElementTache(titreNettoye));
formulaire.reset();
champ.focus();
}
liste.addEventListener("click", traiterClicTache);
formulaire.addEventListener("submit", traiterSoumission);Pour essayer le comportement, chargez la page, ajoutez deux tâches puis cliquez sur leur case ou sur « Supprimer ». Un clic sur une case reste limité à la tâche correspondante ; un clic sur « Supprimer » retire uniquement sa ligne. Le clic sur une commande ne déclenche pas d’action pour un autre élément de la liste, car le gestionnaire vérifie que la commande et la ligne appartiennent bien au conteneur attendu.
Comprendre les garde-fous
La vérification de event.target instanceof Element écarte les cibles qui ne sont pas des éléments du DOM. Le contrôle avec liste.contains() évite de traiter une commande qui ne serait pas dans cette liste. Enfin, la comparaison de data-action à des valeurs connues fait que les autres clics sont ignorés.
À retenir
- La propagation permet à un écouteur placé sur la liste de recevoir les clics des commandes qu’elle contient.
- Utilisez event.target et closest("[data-action]") pour retrouver la commande concernée.
- Reliez la commande à sa tâche avec la ligne marquée par data-id.
- Vérifiez que les éléments trouvés appartiennent bien au conteneur avant d’agir.
Dans la prochaine leçon, vous apprendrez à retirer un écouteur proprement en conservant une référence stable à sa fonction de traitement.