Voir le flux RSS

Bovino

[Actualité] Comprendre la délégation d'événement en JavaScript

Noter ce billet
par , 28/05/2015 à 16h57 (1086 Affichages)
La délégation d'événement, qu'est-ce que c'est ?

La délégation d'événement est une technique assez courante en JavaScript qui consiste à poser des écouteurs d'événement non pas sur l'élément HTML ciblé, mais sur l'un de ses ancêtres dans le DOM.

Comment ça marche ?

Le concept essentiel pour comprendre cette technique est la notion de bouillonnement d'un événement. À de rares exceptions prêt, quasiment tous les événement bouillonnent.
Si l'on se représente une page Web comme une feuille de papier sur laquelle on poserait d'autres feuilles de tailles différentes et placées à des endroits précis selon l'arborescence des balises et la mise en page CSS, on peut se représenter un événement (un clic sur une balise par exemple) comme étant intercepté par l'élément le plus au-dessus de l'empilement (et inversement, le plus profond dans l'arborescence du DOM). C'est donc cet élément qui va recevoir l'événement. Le bouillonnement est le mécanisme qui va faire « remonter » cet événement jusqu'au plus haut niveau de l'arborescence (la première feuille de papier) en passant par tous les éléments se trouvant à l'emplacement où l'événement a été déclenché, permettant ainsi à tous les gestionnaires d'événements associés de se déclencher.

En résumé, si l'on considère le code HTML suivant
Code html : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
<body>
    <div>
        <span>Cliquer ici</span>
    </div>
</body>
et que vous placez un écouteur d'événement clic sur la div, cliquer sur le texte du span déclenchera l'événement de la div bien que ce span « cache » cette dernière. Ceci est possible grâce au bouillonnement.

Pour en revenir à notre délégation d'événement, nous pourrons savoir, avec l'objet Event associé à tout événement, quel élément HTML a réellement déclenché cet événement (Event.target) et si ce dernier est bien celui que nous souhaitons cibler.

À quoi cela peut-il servir ?

Il y a deux cas typiques d'utilisation de la délégation d'événement.

• Lorsque l'on souhaite associer des événements similaires à différents éléments.
Par exemple, imaginons que l'on souhaite afficher un texte initialement masqué en cliquant sur des items d'une liste ordonnée. Plutôt que de définir autant d'événement que d'éléments dans la liste, on pourra n'en utiliser qu'un seul placé sur la balise <ul>
Code html : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
<ul id="maListe">
    <li class="element" data-texte="foo">Item 1</li>
    <li class="element" data-texte="bar">Item 2</li>
    <li class="element" data-texte="baz">Item 3</li>
    <li class="element" data-texte="toto">Item 4</li>
    <li class="element" data-texte="titi">Item 5</li>
    <li class="element" data-texte="tata">Item 6</li>
    <li>Item 7</li>
    <li>Item 8</li>
    <li>Item 9</li>
    <li class="element" data-texte="42">Item 10</li>
</ul>
Code javascript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
document.getElementById('maListe').addEventListener('click', function(e){
    var initElem = e.target;
    if(initElem.className != 'element'){ // Si l'élément n'est pas un de ceux à traiter
        return;
    }
    alert(initElem.dataset.texte);
});
Voir l'exemple sur JSFiddle.

Dans ce code, on cherche à afficher le contenu de l'attribut data-texte pour tous les élément ayant la classe element.
On commence d'abord par récupérer l'élément ayant reçu l'événement. On vérifie ensuite si cet élément est bien du type que l'on cible, si ce n'est pas le cas, on stoppe l'exécution de la fonction, sinon, on affiche le message.

• Lorsque l'on veut prévoir des gestionnaires d'événements sur des éléments n'étant pas encore présents dans la page.
Il est de plus en plus fréquent, avec les interfaces riches et AJAX, que le contenu d'une page évolue en fonction des actions de l'utilisateur.
On peut donc avoir à gérer des événements sur des éléments qui n'existent pas au chargement de la page mais qui sont susceptibles d'y apparaitre.
Seulement, lorsque l'on déclare un gestionnaire, il ne peut s'appliquer qu'à des éléments effectivement présents au moment de la déclaration de l'écouteur : JavaScript n'est pas devin ni prédictif.
Pour cela, on pourra facilement poser le gestionnaire sur un ancêtre connu (et existant) dans lequel seront insérés les futurs éléments.
Code html : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
<div id="parent"></div>
<button>Ajouter un élément</button>
Code javascript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
document.getElementById('ajout').onclick = function(){
    document.getElementById('parent').innerHTML = '<span id="enfant">Elément enfant ajouté dynamiquement</span>';
};
document.getElementById('parent').addEventListener('click', function(e){
    var initElem = e.target;
    if(initElem.id == 'enfant'){
        alert('Vous avez cliqué !');
    }
});
Voir l'exemple sur JSFiddle.

Au moment de déclarer l'événement, l'élément enfant n'existe pas, pourtant, lorsqu'on l'insère dans la page et que l'on clique dessus, le message s'affiche.

Aller plus loin
Il est important, quand on utilise la délégation d'événement, de faire attention que si l'élément ciblé possède des éléments enfants, alors Event.target peut ne pas correspondre à celui ciblé, il faudra dans ce cas remonter l'arborescence jusqu'à l'élément sur lequel le gestionnaire est posé en testant à chaque palier si l'élément en cours est celui recherché.
Code javascript : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
var initElem = false, tmpElem = e.target;
do{
    if(tmpElem.id == 'id_recherche'){
        initElem = tmpElem;
    }
}
while(tmpElem = tmpElem.parentNode && tmpElem != this);
if(initElem){
    // tmpElem ne vaut pas false si on entre dans cette condition
    // donc l'élément recherché a été trouvé
    // on peut donc faire les traitement voulus
}

Il est aussi à noter que la plupart des bibliothèques JavaScript permettent d'utiliser la délégation d'événement.
Par exemple pour jQuery, la syntaxe
Code javascript : Sélectionner tout - Visualiser dans une fenêtre à part
$('#elem1').on('click', '.elems2', callback);
permet de déléguer la gestion des événements clic sur les éléments ayant la classe CSS elems2 sur l'élément dont l'identifiant est elem1.
N'hésitez pas à indiquer dans les commentaires les différentes syntaxes pour les autres frameworks.

Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Viadeo Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Twitter Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Google Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Facebook Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Digg Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Delicious Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog MySpace Envoyer le billet « Comprendre la délégation d'événement en JavaScript » dans le blog Yahoo

Commentaires

  1. Avatar de exe2bin
    • |
    • permalink
    Il est important, quand on utilise la délégation d'événement, de faire attention que si l'élément ciblé possède des éléments enfants, alors Event.target peut ne pas correspondre à celui ciblé, il faudra dans ce cas remonter l'arborescence jusqu'à l'élément sur lequel le gestionnaire est posé en testant à chaque palier si l'élément en cours est celui recherché.
    Bel article mais je n'ai pas tout compris (voir ci-dessus);
    pourrais tu reformulé
  2. Avatar de Bovino
    • |
    • permalink
    Arf... désolé.

    Avec un exemple, ce sera probablement plus compréhensible.

    Imaginons le code HTML
    Code html : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    <div id="A">
        <div class="B">
            <span>Cliquer sur le premier B</span>
        </div>
        <div class="B">
            <span>Cliquer sur le second B</span>
        </div>
    </div>
    Imaginons que tu veuilles traiter les clics sur les éléments ayant la classe B, tu poses donc le gestionnaire sur la div A.
    Sauf que lorsque tu cliques sur le texte, c'est la balise <span> qui va déclencher l'événement et ce sera donc elle qui sera référencée par event.target.
    Donc si tu fais le test if(initElem.className != 'B'){ ... }, il sera vrai. Il est donc nécessaire de vérifier si l'un des ancêtres de event.target possède cette classe.
  3. Avatar de SylvainPV
    • |
    • permalink
    Merci Bovino, c'est vrai que c'est un sujet courant et qu'on avait rien comme ressources dessus.

    J'en profite pour te demander ton avis sur un micro-débat: penses-tu que cela ait un sens d'utiliser exclusivement de la délégation d'évènements sur l'élément de plus haut niveau (document.body) pour gérer tous les évènements d'une application ? Certains ont émis ce genre d'idées, notamment en parallèle avec l'utilisation de React et de l'architecture Flux, pour remplacer l'usage classique de la phase de propagation des évènements qu'ils trouvent confusante par leur propre système dispatcher (qui peut se résumer en gros à un pub/sub sur les composants React). Je suis pas un fada de React ni du fait de réinventer la roue, mais je dois avouer m'être demandé plusieurs s'il n'y avait pas une meilleure façon de faire que ces event.preventDefault() && event.stopImmediatePropagation() par ci par là.
    Mis à jour 31/05/2015 à 16h53 par SylvainPV
  4. Avatar de Bovino
    • |
    • permalink
    Salut Sylvain.

    Je ne connais pas du tout React, mais je sais qu'affecter tous les événements sur le body a été la première approche utilisée par jQuery avec la méthode .live(). Ils ont changé d'optique en considérant que le fait de ne pas pouvoir stopper la propagation de l'événement pouvait être problématique. J'aurais tendance à me ranger à cet avis en considérant aussi que devoir gérer tous les événements d'un même type sur la page est souvent un peu surdimensionné comparé au besoin réel la plupart du temps.
    Par exemple, si on doit gérer le clic sur un texte qui apparaitra au retour d'une requête AJAX ne nécessite pas pour autant que l'on gère tous les clics de toute la page constamment.
    Donc à mon sens, l'idéal est de placer le gestionnaire sur le plus proche parent connu de l'élément visé.
  5. Avatar de Paul_Le_Heros
    • |
    • permalink
    Bonjour,
    Merci Bovino, pour avoir fait avancer le schmilblick...
    Je me sens moins seul quand je lis "SylvainPV" :
    une meilleure façon de faire que ces event.preventDefault() && event.stopImmediatePropagation() par ci par là
    Il n'a pas l'air d'être satisfait de lui : « par ci par là »...
    J'ai bien de la peine avec les événements et leurs handlers. J'ai beau (mal) chercher, je ne trouve rien sur le net pour clarifier définitivement cette situation. J'aime bien JQuery, alors si vous connaissez un tuto sur "le traitement des événements avec JQuery", dites moi ! Poyr vous donner une idée de l'aviron que j'ai pratiqué : il n'y a pas si longtemps que je m'usais à utiliser return !0; ou return !1; en sortie de handler pour signifier la fin ou non du traitement de l'événement, comme indiqué dans ma doc JS de Mathusalem (-: l'originale, peut-être !)
    Bon courage à tous,
    Paul