preventDefault
et stopPropagation
: quand utiliser l'une ou l'autre et quelle est la fonction exacte de chaque méthode.
Event.stopPropagation() et Event.preventDefault()
La gestion des événements JavaScript est souvent simple. Cela est particulièrement vrai lorsqu'il s'agit d'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. Il s'agit généralement du moment où les développeurs utilisent 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 à quoi sert chaque méthode, quand l'utiliser et vous fournir divers exemples fonctionnels à 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 récents prennent en charge la capture d'événements, mais elle est très rarement utilisée par les développeurs.
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'une valeur booléenne, mais tous les navigateurs récents 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 propagera ensuite à ses descendants comme suit:
window
=> document
=> <html>
=> <body>
=>, etc. jusqu'à ce qu'elle atteigne 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 commence toujours à window
et commence son parcours comme nous venons de le voir.
Dans notre exemple, l'événement de clic se propage (il s'agit d'un mot important, car il est 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 qu'un élément é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 au document
et le navigateur demande : "Un événement de clic est-il à l'écoute sur le document
lors de 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>
pendant la phase de capture ?" Si tel est le cas, les gestionnaires d'événements appropriés se déclencheront.
Ensuite, l'événement se propage à l'élément <body>
, et le navigateur demande : "Est-ce que quelque chose écoute un événement de clic sur l'élément <body>
lors de la phase de capture ?" Si tel est le cas, les gestionnaires d'événements appropriés se déclencheront.
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 : "Quel élément écoute-t-il un événement de clic sur l'élément #C
lors de 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. Nous n'avons pas encore fini. Le processus se poursuit, mais passe maintenant à la phase de bouillonnement.
Bulles d'événements
Le navigateur vous demande :
"Est-ce qu'un élément écoute un événement de clic sur #C
pendant la phase d'ébullition ?" 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 parcours 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 à l'autre (et il s'agit généralement de la phase d'ébullition).
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 utile de voir le chemin qu'emprunte 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 le résultat de la console.
event.stopPropagation()
Maintenant que nous avons bien compris l'origine des événements et la façon dont ils se déplacent (c'est-à-dire se propager) dans le DOM lors de la phase de capture et d'ébullition, nous pouvons désormais nous pencher sur 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 tout 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 directions (capture et bulles). 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 lors de la phase d'ébullition, elle aura déjà passé la phase de capture, mais elle cessera de "bouillir" à partir du point où vous l'avez appelée.
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 jouer avec ce jeu 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 jouer avec ce jeu 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 le résultat 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 (bouillage), 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. Lorsque cela se produit (dans la plupart des navigateurs), les gestionnaires d'événements sont exécutés dans l'ordre dans lequel ils ont été câblé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()
? On dirait que ça fait
quelque chose de similaire. Est-ce le cas ?
Pas vraiment. Bien qu'ils soient souvent confondus, ils n'ont en fait 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 aussi 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 se passe-t-il 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 le résultat de la console (et le fait que vous n'êtes pas redirigé vers le site Web 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 n'arrive pas à toutes les lister, et parfois il faut juste expérimenter 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, appelezaddEventListener()
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" appelée en appuyant sur le bouton de la souris.Élément
<input>
+ événement "keypress" :preventDefault()
pour cette combinaison empêche les caractères saisis par l'utilisateur d'atteindre l'élément d'entrée (mais ne le faites pas, car cela a rarement, voire jamais, une raison valable).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 totalement inutile:
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 ça (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 cette situation.
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 la plupart du temps) 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.