Présentation détaillée des événements JavaScript

preventDefault et stopPropagation: quand utiliser l'une ou l'autre et quelle est la fonction exacte de chaque méthode.

La gestion des événements JavaScript est souvent simple. C'est particulièrement vrai lorsque vous travaillez avec une structure HTML simple (relativement plate). Les choses deviennent un peu plus complexes lorsque les événements se propagent (ou se propagent) à travers une hiérarchie d'éléments. C'est généralement lorsque les développeurs se tournent vers stopPropagation() et/ou preventDefault() pour résoudre les problèmes qu'ils rencontrent. Si vous vous êtes déjà dit : "Je vais juste essayer preventDefault() et si ça ne fonctionne pas, je vais essayer stopPropagation() et si ça ne fonctionne pas, je vais essayer les deux", cet article est fait pour vous. Je vais vous expliquer exactement ce que fait chaque méthode, quand utiliser telle ou telle méthode, et vous fournir une variété d'exemples pratiques à explorer. Mon objectif est de mettre fin à votre confusion une fois pour toutes.

Avant de nous plonger dans le sujet, il est important de revenir brièvement sur les deux types de gestion des événements possibles en JavaScript (dans tous les navigateurs modernes, Internet Explorer avant la version 9 ne prenait pas du tout en charge la capture d'événements).

Styles d'événements (capture et propagation)

Tous les navigateurs modernes sont compatibles avec la capture d'événements, mais les développeurs l'utilisent très rarement. Il est intéressant de noter qu'il s'agissait de la seule forme d'événements prise en charge par Netscape à l'origine. Le plus grand rival de Netscape, Microsoft Internet Explorer, n'était pas du tout compatible avec la capture d'événements, mais ne prenait en charge qu'un autre style d'événement appelé "bubbling". Lorsque le W3C a été créé, il a trouvé du mérite dans les deux styles d'événements et a déclaré que les navigateurs devaient les accepter tous les deux, via un troisième paramètre de la méthode addEventListener. À l'origine, ce paramètre n'était qu'un booléen, mais tous les navigateurs modernes acceptent un objet options comme troisième paramètre, que vous pouvez utiliser pour spécifier (entre autres) si vous souhaitez utiliser la capture d'événements ou non:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

Notez que l'objet options est facultatif, tout comme sa propriété capture. Si l'un d'eux est omis, la valeur par défaut de capture est false, ce qui signifie que la propagation des événements sera utilisée.

Capture d'événements

Que signifie-t-il si votre gestionnaire d'événements est en "écoute pendant la phase de capture" ? Pour comprendre cela, nous devons savoir comment les événements se produisent et comment ils se propagent. Ce qui suit est vrai pour tous les événements, même si vous, en tant que développeur, ne les exploitez pas, ne vous en souciez pas ou n'y pensez pas.

Tous les événements commencent à la fenêtre et passent d'abord par la phase de capture. Cela signifie que lorsqu'un événement est distribué, il démarre la fenêtre et se déplace d'abord vers son élément cible. Cela se produit même si vous n'écoutez que pendant la phase de bouillonnement. Prenons l'exemple de balisage et de code JavaScript suivants:

<html>
 
<body>
   
<div id="A">
     
<div id="B">
       
<div id="C"></div>
     
</div>
   
</div>
 
</body>
</html>
document.getElementById('C').addEventListener(
 
'click',
 
function (e) {
    console
.log('#C was clicked');
 
},
 
true,
);

Lorsqu'un utilisateur clique sur l'élément #C, un événement provenant de window est distribué. Cet événement se propage ensuite à ses descendants comme suit:

window => document => <html> => <body> => et ainsi de suite, jusqu'à atteindre la cible.

Peu importe si aucun élément n'écoute un événement de clic au niveau de l'élément window, document, <html> ou <body> (ou de tout autre élément sur le chemin de sa cible). Un événement provient toujours du window et commence son parcours comme décrit précédemment.

Dans notre exemple, l'événement de clic se propage (il s'agit d'un mot important, car il sera directement lié au fonctionnement de la méthode stopPropagation() et sera expliqué plus loin dans ce document) de window à son élément cible (dans ce cas, #C) via chaque élément entre window et #C.

Cela signifie que l'événement de clic commencera à window et que le navigateur posera les questions suivantes:

"Est-ce que quelque chose écoute un événement de clic sur le window pendant la phase de capture ?" Si tel est le cas, les gestionnaires d'événements appropriés se déclenchent. Dans notre exemple, aucun élément n'est défini, de sorte qu'aucun gestionnaire ne se déclenche.

Ensuite, l'événement se propage vers document, et le navigateur demande: "Quelqu'un écoute-t-il un événement de clic sur document pendant la phase de capture ?" Si c'est le cas, les gestionnaires d'événements appropriés se déclenchent.

Ensuite, l'événement se propage vers l'élément <html>, et le navigateur demande: "Quel élément est à l'écoute d'un clic sur l'élément <html> lors de la phase de capture ?" Si c'est le cas, les gestionnaires d'événements appropriés seront déclenchés.

Ensuite, l'événement se propage vers l'élément <body>, et le navigateur demande: "Quelqu'un écoute-t-il un événement de clic sur l'élément <body> pendant la phase de capture ?" Si tel est le cas, les gestionnaires d'événements appropriés se déclenchent.

L'événement se propage ensuite vers l'élément #A. À nouveau, le navigateur demande: "Quelqu'un écoute-t-il un événement de clic sur #A pendant la phase de capture ? Si oui, les gestionnaires d'événements appropriés seront déclenchés.

Ensuite, l'événement se propage vers l'élément #B (et la même question est posée).

Enfin, l'événement atteint sa cible et le navigateur demande: "Quelqu'un écoute-t-il un événement de clic sur l'élément #C pendant la phase de capture ?" La réponse est oui. Cette brève période pendant laquelle l'événement se trouve à la cible est appelée "phase cible". À ce stade, le gestionnaire d'événements se déclenche, le navigateur console.log "#C a été cliqué", et nous avons terminé, n'est-ce pas ? Faux. Ce n'est pas tout. Le processus se poursuit, mais passe maintenant à la phase de bouillonnement.

Événement remonté

Le navigateur vous demande:

"Est-ce que quelque chose écoute un événement de clic sur #C pendant la phase de bouillonnement ?" Prêtez une attention toute particulière à ce point. Il est tout à fait possible d'écouter les clics (ou tout autre type d'événement) à la fois pendant les phases de capture et de bouillonnement. Et si vous avez câblé des gestionnaires d'événements dans les deux phases (par exemple, en appelant .addEventListener() deux fois, une fois avec capture = true et une fois avec capture = false), alors oui, les deux gestionnaires d'événements se déclencheront absolument pour le même élément. Il est toutefois important de noter qu'ils se déclenchent à différentes phases (l'une lors de la phase de capture et l'autre lors de la phase de bouillonnement).

Ensuite, l'événement se propage (plus communément appelé "bouilonnement", car il semble que l'événement se déplace "vers le haut" de l'arborescence DOM) vers son élément parent, #B, et le navigateur demande: "Quel élément écoute-t-il les événements de clic sur #B pendant la phase de bouillonnement ?" Dans notre exemple, rien n'est défini, donc aucun gestionnaire ne se déclenche.

Ensuite, l'événement remonte vers #A et le navigateur demande: "Est-ce que quelque chose écoute les événements de clic sur #A pendant la phase de bouillonnement ?"

Ensuite, l'événement remonte vers <body>: "Est-ce que quelque chose écoute les événements de clic sur l'élément <body> pendant la phase de bouillonnement ?"

Ensuite, l'élément <html>: "Est-ce que quelque chose écoute les événements de clic sur l'élément <html> pendant la phase de bouillonnement ?

Ensuite, le document: "Est-ce que quelque chose écoute les événements de clic sur le document pendant la phase de bouillonnement ?"

Enfin, window: "Est-ce que quelque chose écoute les événements de clic sur la fenêtre pendant la phase de bouillonnement ?"

Ouf ! Ce fut un long voyage, et notre événement est probablement très fatigué à présent, mais croyez-le ou non, c'est le voyage que chaque événement doit suivre. La plupart du temps, cela n'est jamais remarqué, car les développeurs ne s'intéressent généralement qu'à une phase d'événement ou à une autre (et il s'agit généralement de la phase de bouillonnement).

Il est utile de passer un peu de temps à jouer avec la capture et la propagation d'événements, et à consigner des notes dans la console lorsque les gestionnaires se déclenchent. Il est très intéressant de voir le chemin suivi par un événement. Voici un exemple qui écoute chaque élément dans les deux phases.

<html>
 
<body>
   
<div id="A">
     
<div id="B">
       
<div id="C"></div>
     
</div>
   
</div>
 
</body>
</html>
document.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on document in capturing phase');
 
},
 
true,
);
// document.documentElement == <html>
document
.documentElement.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <html> in capturing phase');
 
},
 
true,
);
document
.body.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <body> in capturing phase');
 
},
 
true,
);
document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #A in capturing phase');
 
},
 
true,
);
document
.getElementById('B').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #B in capturing phase');
 
},
 
true,
);
document
.getElementById('C').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #C in capturing phase');
 
},
 
true,
);

document
.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on document in bubbling phase');
 
},
 
false,
);
// document.documentElement == <html>
document
.documentElement.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <html> in bubbling phase');
 
},
 
false,
);
document
.body.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <body> in bubbling phase');
 
},
 
false,
);
document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #A in bubbling phase');
 
},
 
false,
);
document
.getElementById('B').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #B in bubbling phase');
 
},
 
false,
);
document
.getElementById('C').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #C in bubbling phase');
 
},
 
false,
);

La sortie de la console dépend de l'élément sur lequel vous cliquez. Si vous cliquez sur l'élément le plus profond de l'arborescence DOM (l'élément #C), vous verrez tous ces gestionnaires d'événements se déclencher. Avec un peu de style CSS pour identifier plus facilement les éléments, voici l'élément #C de sortie de la console (avec une capture d'écran également):

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"

Vous pouvez tester cette fonctionnalité de manière interactive dans la démonstration en direct ci-dessous. Cliquez sur l'élément #C et observez la sortie de la console.

event.stopPropagation()

Maintenant que vous savez d'où proviennent les événements et comment ils se propagent (c'est-à-dire se propagent) dans le DOM à la fois pendant la phase de capture et la phase de bouillonnement, vous pouvez maintenant vous intéresser à event.stopPropagation().

La méthode stopPropagation() peut être appelée sur la plupart des événements DOM natifs. Je dis "la plupart", car il y en a quelques-uns pour lesquels l'appel de cette méthode ne sert à rien (car l'événement ne se propage pas au départ). Les événements tels que focus, blur, load, scroll et quelques autres entrent dans cette catégorie. Vous pouvez appeler stopPropagation(), mais rien d'intéressant ne se passera, car ces événements ne se propagent pas.

Mais que fait stopPropagation ?

Il fait à peu près ce qu'il dit. Lorsque vous l'appelez, l'événement cesse de se propager à tous les éléments vers lesquels il se serait autrement propagé. Cela est vrai dans les deux sens (capture et bulle). Par conséquent, si vous appelez stopPropagation() n'importe où dans la phase de capture, l'événement n'atteindra jamais la phase cible ni la phase de bouillonnement. Si vous l'appelez pendant la phase de remontée, il aura déjà passé la phase de capture, mais il cessera de remonter à partir du point où vous l'avez appelé.

Pour revenir à notre exemple de balisage, que se passerait-il si nous appelions stopPropagation() lors de la phase de capture au niveau de l'élément #B ?

Vous obtiendrez le résultat suivant:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"

Vous pouvez tester cette fonctionnalité de manière interactive dans la démonstration en direct ci-dessous. Cliquez sur l'élément #C dans la démonstration en direct et observez la sortie de la console.

Que diriez-vous d'arrêter la propagation à #A pendant la phase de bouillonnement ? Vous obtiendrez le résultat suivant:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"

Vous pouvez tester cette fonctionnalité de manière interactive dans la démonstration en direct ci-dessous. Cliquez sur l'élément #C dans la démonstration en direct et observez la sortie de la console.

Encore une fois, juste pour le plaisir. Que se passe-t-il si nous appelons stopPropagation() dans la phase cible pour #C ? Rappelez-vous que la "phase cible" est le nom donné à la période pendant laquelle l'événement est à sa cible. Vous obtiendrez le résultat suivant:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"

Notez que le gestionnaire d'événements pour #C dans lequel nous enregistrons "cliquer sur #C dans la phase de capture" s'exécute toujours, mais celui dans lequel nous enregistrons "cliquer sur #C dans la phase de bubbling" ne le fait pas. Cela devrait être parfaitement logique. Nous avons appelé stopPropagation() à partir de l'ancien, c'est donc à ce stade que la propagation de l'événement cessera.

Vous pouvez tester cette fonctionnalité de manière interactive dans la démonstration en direct ci-dessous. Cliquez sur l'élément #C dans la démonstration en direct et observez la sortie de la console.

Dans chacune de ces démonstrations en direct, je vous encourage à jouer avec. Essayez de cliquer uniquement sur l'élément #A ou body. Essayez de prédire ce qui va se passer, puis vérifiez si vous avez raison. À ce stade, vous devriez pouvoir faire des prédictions assez précises.

event.stopImmediatePropagation()

De quoi s'agit-il ? Il est semblable à stopPropagation, mais au lieu d'empêcher un événement de se propager aux descendants (capture) ou aux ancêtres (bubbling), cette méthode ne s'applique que lorsque vous avez plusieurs gestionnaires d'événements connectés à un seul élément. Étant donné que addEventListener() est compatible avec un style d'événements multicast, il est tout à fait possible de connecter un gestionnaire d'événements à un seul élément plusieurs fois. Dans ce cas (dans la plupart des navigateurs), les gestionnaires d'événements sont exécutés dans l'ordre dans lequel ils ont été connectés. L'appel de stopImmediatePropagation() empêche le déclenchement de tout gestionnaire ultérieur. Prenons l'exemple suivant :

<html>
 
<body>
   
<div id="A">I am the #A element</div>
 
</body>
</html>
document.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('When #A is clicked, I shall run first!');
 
},
 
false,
);

document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('When #A is clicked, I shall run second!');
    e
.stopImmediatePropagation();
 
},
 
false,
);

document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
 
},
 
false,
);

L'exemple ci-dessus génère la sortie de console suivante:

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

Notez que le troisième gestionnaire d'événements ne s'exécute jamais, car le deuxième gestionnaire d'événements appelle e.stopImmediatePropagation(). Si nous avions appelé e.stopPropagation() à la place, le troisième gestionnaire s'exécuterait toujours.

event.preventDefault()

Si stopPropagation() empêche un événement de se propager "vers le bas" (capture) ou "vers le haut" (bubbling), que fait preventDefault() ? Il semble que ce soit le cas. Est-ce le cas ?

Pas vraiment. Bien que ces deux termes soient souvent confondus, ils n'ont en réalité pas grand-chose à voir l'un avec l'autre. Lorsque vous voyez preventDefault(), ajoutez mentalement le mot "action". Pensez à "empêcher l'action par défaut".

Quelle action par défaut pouvez-vous demander ? Malheureusement, la réponse à cette question n'est pas tout à fait claire, car elle dépend fortement de la combinaison élément + événement en question. Pour compliquer encore les choses, il n'y a parfois pas d'action par défaut du tout.

Commençons par un exemple très simple à comprendre. Que vous attendez-vous à se passer lorsque vous cliquez sur un lien sur une page Web ? Évidemment, vous vous attendez à ce que le navigateur accède à l'URL spécifiée par ce lien. Dans ce cas, l'élément est une balise d'ancrage et l'événement est un événement de clic. Cette combinaison (<a> + click) a pour "action par défaut" la navigation vers l'href du lien. Que faire si vous souhaitez empêcher le navigateur d'effectuer cette action par défaut ? Par exemple, supposons que vous souhaitiez empêcher le navigateur d'accéder à l'URL spécifiée par l'attribut href de l'élément <a>. C'est ce que preventDefault() fera pour vous. Considérez l'exemple suivant :

<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
 
'click',
 
function (e) {
    e
.preventDefault();
    console
.log('Maybe we should just play some of their music right here instead?');
 
},
 
false,
);

Vous pouvez tester cette fonctionnalité de manière interactive dans la démonstration en direct ci-dessous. Cliquez sur le lien The Avett Brothers et observez la sortie de la console (et le fait que vous n'êtes pas redirigé vers le site Web des Avett Brothers).

Normalement, un clic sur le lien "The Avett Brothers" permet d'accéder à www.theavettbrothers.com. Dans ce cas, nous avons connecté un gestionnaire d'événements de clic à l'élément <a> et spécifié que l'action par défaut doit être empêchée. Par conséquent, lorsqu'un utilisateur clique sur ce lien, il n'est redirigé vers aucune page. La console enregistre simplement "Peut-être devrions-nous simplement diffuser de la musique ici ?".

Quelles autres combinaisons d'éléments/d'événements vous permettent d'empêcher l'action par défaut ? Je ne peux pas tous les énumérer, et parfois, vous devez simplement tester pour voir. Voici quelques exemples:

  • Élément <form> + événement "submit" : preventDefault() pour cette combinaison, l'envoi d'un formulaire est empêché. Cela est utile si vous souhaitez effectuer une validation et, en cas d'échec, vous pouvez appeler preventDefault de manière conditionnelle pour empêcher l'envoi du formulaire.

  • Élément <a> + événement "click" : preventDefault() pour cette combinaison, le navigateur ne peut pas accéder à l'URL spécifiée dans l'attribut href de l'élément <a>.

  • document + Événement "mousewheel" : preventDefault() pour cette combinaison empêche le défilement de la page avec la molette de la souris (le défilement avec le clavier fonctionne toujours).
    ↜ Pour ce faire, appelez addEventListener() avec { passive: false }.

  • document + Événement "keydown" : preventDefault() pour cette combinaison est mortel. Cela rend la page pratiquement inutilisable, car elle empêche le défilement, la tabulation et la mise en surbrillance au clavier.

  • document + Événement "mousedown" : preventDefault() pour cette combinaison empêche la mise en surbrillance du texte avec la souris et toute autre action "par défaut" que l'on pourrait appeler avec un clic de souris.

  • Élément <input> + événement "keypress" : preventDefault() pour cette combinaison, les caractères saisis par l'utilisateur ne peuvent pas atteindre l'élément de saisie (mais ne faites pas cela ; il y a rarement, voire jamais, une raison valable pour cela).

  • document + Événement "contextmenu" : preventDefault() pour cette combinaison, le menu contextuel du navigateur natif ne s'affiche pas lorsqu'un utilisateur effectue un clic droit ou un appui prolongé (ou toute autre façon dont un menu contextuel peut s'afficher).

Cette liste n'est en aucun cas exhaustive, mais elle vous donne une bonne idée de la façon dont preventDefault() peut être utilisé.

Une farce amusante ?

Que se passe-t-il si vous stopPropagation() et preventDefault() pendant la phase de capture, en commençant par le document ? Hilarité assurée ! L'extrait de code suivant rend n'importe quelle page Web presque complètement inutilisable:

function preventEverything(e) {
  e
.preventDefault();
  e
.stopPropagation();
  e
.stopImmediatePropagation();
}

document
.addEventListener('click', preventEverything, true);
document
.addEventListener('keydown', preventEverything, true);
document
.addEventListener('mousedown', preventEverything, true);
document
.addEventListener('contextmenu', preventEverything, true);
document
.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });

Je ne sais pas vraiment pourquoi vous voudriez faire cela (sauf peut-être pour faire une blague à quelqu'un), mais il est utile de réfléchir à ce qui se passe ici et de comprendre pourquoi cela crée la situation actuelle.

Tous les événements proviennent de window. Dans cet extrait, nous arrêtons tous les événements click, keydown, mousedown, contextmenu et mousewheel pour qu'ils n'atteignent jamais les éléments qui pourraient les écouter. Nous appelons également stopImmediatePropagation afin que tous les gestionnaires connectés au document après celui-ci soient également bloqués.

Notez que stopPropagation() et stopImmediatePropagation() ne sont pas (du moins pas principalement) ce qui rend la page inutile. Ils empêchent simplement les événements d'atteindre leur destination.

Nous appelons également preventDefault(), qui, comme vous vous en souvenez, empêche l'action par défaut. Par conséquent, toutes les actions par défaut (comme le défilement de la molette de la souris, le défilement au clavier ou la sélection ou la tabulation, le clic sur un lien, l'affichage du menu contextuel, etc.) sont empêchées, ce qui laisse la page dans un état assez inutile.

Démonstrations en direct

Pour revoir tous les exemples de cet article au même endroit, regardez la démonstration intégrée ci-dessous.

Remerciements

Image héros par Tom Wilson sur Unsplash.