Délégation d'événements JavaScript
Apprenez la délégation d'événements JavaScript : bubbling, écouteur parent, event.target + closest(), attributs data-* et pièges courants.
Maîtriser la délégation d'événements en JavaScript
La délégation d'événements est une technique puissante en JavaScript pour gérer les événements de manière efficace, en particulier lorsqu'il s'agit de nombreux éléments similaires ou d'éléments ajoutés dynamiquement. Ce guide explique ce qu'est la délégation d'événements, pourquoi elle est utile, comment elle s'appuie sur le bubbling, ainsi que les patterns et les pièges à connaître pour l'utiliser de façon fiable.
Cette page couvre :
- Ce qu'est la délégation d'événements et le mécanisme de bubbling dont elle dépend
- Pourquoi elle économise de la mémoire et gère les éléments ajoutés dynamiquement
- Le pattern robuste
event.target+closest()(et pourquoitagNameseul est fragile) - La lecture de données depuis les éléments cliqués avec les attributs
data-* - Les pièges courants, notamment les événements qui ne remontent pas
- Quand ne pas utiliser la délégation
Comprendre la délégation d'événements
La délégation d'événements tire parti du fait que la plupart des événements remontent dans le DOM : lorsqu'un événement se déclenche sur un élément, il se déclenche ensuite sur le parent de cet élément, puis sur son grand-parent, et ainsi de suite jusqu'à document. Au lieu d'attacher un écouteur d'événement à chaque élément individuellement, vous attachez un seul écouteur à un ancêtre commun. Cet unique écouteur gère tous les événements qui remontent depuis n'importe quel descendant.
Si le bubbling est une nouveauté pour vous, lisez d'abord Bubbling et capturing — c'est le mécanisme sur lequel repose toute la technique.
Avantages de la délégation d'événements
- Efficacité mémoire : Réduit le nombre d'écouteurs d'événements dans votre application, ce qui peut économiser de la mémoire et améliorer les performances, surtout avec un grand nombre d'éléments.
- Éléments dynamiques : Gère les événements sur des éléments ajoutés dynamiquement au DOM après le chargement initial de la page.
- Simplicité : Simplifie la gestion des écouteurs d'événements, notamment lorsque de nombreux éléments se comportent de façon similaire.
Comment ça fonctionne
La délégation d'événements exploite la phase de bubbling. Un événement déclenché sur un enfant remonte vers ses ancêtres, où un seul écouteur le capture. Deux propriétés sont essentielles dans le gestionnaire :
event.target— l'élément réel avec lequel l'utilisateur a interagi (l'élément le plus profond). C'est ce que vous examinez pour décider quel élément a été cliqué.event.currentTarget— l'élément auquel l'écouteur est attaché (le parent). Dans une fonction normale, c'est la même valeur quethis.
Le gestionnaire lit event.target, détermine s'il appartient à un enfant pertinent, et agit en conséquence.
Exemples pratiques de délégation d'événements
Voici quelques exemples pratiques montrant comment implémenter la délégation d'événements dans des scénarios réels :
Exemple 1 : Gérer les clics sur une liste
Imaginez que vous ayez une liste d'éléments et que vous souhaitiez gérer les clics sur chaque élément sans attacher un écouteur d'événement à chaque élément de liste individuellement.
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<!-- More items can be added dynamically -->
</ul>
<script>
document.getElementById('myList').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
alert('You clicked on ' + event.target.textContent);
}
});
</script>Explication :
- L'écouteur d'événement est ajouté à l'élément
<ul>. - Lorsqu'un élément de liste (
<li>) est cliqué, l'événement remonte jusqu'à<ul>, et l'écouteur d'événement est déclenché. - La propriété
event.targetest vérifiée pour s'assurer que le clic provient bien d'un élément de liste.
Exemple 2 : Gérer les clics sur des boutons dans une interface dynamique
Dans une interface avec des boutons ajoutés dynamiquement, la délégation d'événements peut être utilisée pour gérer les clics sur les boutons de manière efficace.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dynamic Button Feedback Example</title>
<style>
#feedback {
color: blue;
margin-top: 10px;
}
</style>
</head>
<body>
<div id="buttonContainer">
<!-- Buttons can be added or removed dynamically -->
<button>Action 1</button>
<button>Action 2</button>
</div>
<div id="feedback"></div>
<script>
const buttonContainer = document.getElementById('buttonContainer');
const feedback = document.getElementById('feedback');
buttonContainer.addEventListener('click', function(event) {
if (event.target.tagName === 'BUTTON') {
feedback.textContent = 'Button clicked: ' + event.target.textContent;
}
});
</script>
</body>
</html>Explication :
- Un seul écouteur d'événement
clickest ajouté à un élément conteneur. - Il vérifie si l'élément cliqué est un bouton et réagit à l'événement de clic selon le bouton cliqué.
Un pattern plus robuste : utiliser closest()
Vérifier event.target.tagName ne fonctionne que lorsque l'utilisateur clique exactement sur l'élément attendu. Mais les boutons et les éléments de liste contiennent souvent du balisage imbriqué — une icône, un <span>, un <strong>. Si l'utilisateur clique sur cet élément interne, event.target sera le <span>, pas le <button>, et une vérification tagName === 'BUTTON' échouera silencieusement.
Element.closest(selector) résout ce problème. Il remonte depuis event.target et retourne l'ancêtre le plus proche (l'élément lui-même inclus) qui correspond à un sélecteur CSS, ou null si aucun ne correspond. Cela rend la délégation résistante au contenu imbriqué.
<ul id="menu">
<li class="menu-item"><span>Profile</span></li>
<li class="menu-item"><span>Settings</span></li>
<li class="menu-item"><span>Logout</span></li>
</ul>
<script>
document.getElementById('menu').addEventListener('click', function (event) {
// Find the .menu-item ancestor, even if a <span> was clicked.
const item = event.target.closest('.menu-item');
// closest() can return null (e.g. a click on padding around the items),
// and we should ignore clicks outside this list entirely.
if (!item || !this.contains(item)) return;
console.log('Selected:', item.textContent.trim());
});
</script>La garde !item || !this.contains(item) est importante : closest() continue de remonter au-delà du conteneur, donc sans this.contains(item) vous pourriez correspondre à un élément en dehors de la liste. this ici est le <ul> (le currentTarget).
Lire des données avec les attributs data-*
Une fois que vous savez quel élément a été cliqué, vous avez généralement besoin de données le concernant — un id, un nom d'action, un index de ligne. Coder en dur la logique par élément va à l'encontre de l'objectif de la délégation. À la place, stockez les données sur chaque élément avec un attribut data-* et lisez-les depuis dataset.
<div id="toolbar">
<button data-action="save">Save</button>
<button data-action="delete">Delete</button>
<button data-action="share">Share</button>
</div>
<script>
const actions = {
save: () => console.log('Saving...'),
delete: () => console.log('Deleting...'),
share: () => console.log('Sharing...'),
};
document.getElementById('toolbar').addEventListener('click', function (event) {
const button = event.target.closest('button[data-action]');
if (!button) return;
const handler = actions[button.dataset.action];
if (handler) handler();
});
</script>Ce pattern de "table d'actions" passe à l'échelle proprement : ajoutez un nouveau bouton avec un data-action et une entrée correspondante dans l'objet actions — pas de nouveaux écouteurs, pas de chaîne if/else.
Pièges et mises en garde
La délégation est puissante mais comporte des angles tranchants. Gardez ces points à l'esprit :
- Tous les événements ne remontent pas.
focus,blur,mouseenteretmouseleavene remontent pas, donc la délégation ne les capture pas sur un parent. Utilisez plutôt leurs équivalents qui remontent :focusin/focusoutpour le focus, etmouseover/mouseoutpour le survol (puis filtrez avecevent.target). event.targetvsevent.currentTarget.targetest l'origine de l'événement ;currentTarget(etthisdans une fonction normale) est l'élément sur lequel l'écouteur est attaché. Les confondre est le bug de délégation le plus courant.- Les fonctions fléchées et
this. Une fonction fléchée ne lie pas son proprethis, doncthisne sera pas le conteneur. Utilisezevent.currentTargetà la place si vous écrivez le gestionnaire comme une fonction fléchée. - La propagation arrêtée. Si un gestionnaire enfant appelle
event.stopPropagation(), l'événement n'atteint jamais votre écouteur délégué. ÉvitezstopPropagation()sauf si vous en avez vraiment besoin. - Conteneurs trop larges. Attacher l'écouteur à
documentpour tout signifie que chaque clic exécute votre gestionnaire. Limitez l'écouteur à l'ancêtre le plus petit et le plus pertinent.
Quand l'utiliser (et quand ne pas l'utiliser)
| Situation | Délégation ? |
|---|---|
| Nombreux enfants similaires (liste, tableau, grille) | Oui — un seul écouteur pour tous |
| Enfants ajoutés/supprimés dynamiquement | Oui — pas besoin de (re)lier |
| Un seul élément unique | Non — liez directement ; c'est plus simple |
Événements non-remontants (focus, blur) | Non — utilisez focusin/focusout ou liez directement |
Vous devez appeler preventDefault() tôt | Souvent possible, mais liez directement pour les actions par défaut que vous devez contrôler précisément |
Pour les événements que vous créez vous-même plutôt que ceux intégrés au navigateur, consultez Dispatching custom events.
Conclusion
La délégation d'événements est une technique essentielle pour une gestion efficace des événements en JavaScript, particulièrement utile dans les applications comportant de nombreux éléments ou du contenu dynamique. En comprenant et en utilisant la délégation d'événements, les développeurs peuvent améliorer considérablement les performances et la maintenabilité de leurs applications, les rendant plus réactives et plus faciles à gérer.