preventDefault
y stopPropagation
: cuándo usar cada uno y qué hace exactamente cada método
Event.stopPropagation() y Event.preventDefault()
El manejo de eventos de JavaScript suele ser sencillo. Esto es especialmente cierto cuando se trata de una estructura HTML simple (relativamente plana). Sin embargo, las cosas se complican un poco cuando los eventos se propagan a través de una jerarquía de elementos. Por lo general, esto es cuando los desarrolladores se comunican con stopPropagation()
o preventDefault()
para resolver los problemas que tienen. Si alguna vez pensaste: "Voy a probar preventDefault()
y, si no funciona, probaré stopPropagation()
y, si no funciona, probaré ambas", este artículo es para ti. Te explicaré exactamente qué hace cada método, cuándo usarlo y te proporcionaré una variedad de ejemplos funcionales para que los explores. Mi objetivo es acabar con tu confusión de una vez por todas.
Sin embargo, antes de profundizar demasiado, es importante mencionar brevemente los dos tipos de control de eventos posibles en JavaScript (en todos los navegadores modernos, Internet Explorer antes de la versión 9 no admitía la captura de eventos).
Estilos de eventos (captura y burbujeo)
Todos los navegadores modernos admiten la captura de eventos, pero los desarrolladores rara vez la usan.
Curiosamente, era la única forma de eventos que Netscape admitía originalmente. El rival más grande de Netscape, Microsoft Internet Explorer, no admitía la captura de eventos en absoluto, sino que solo admitía otro estilo de eventos llamado burbujeo de eventos. Cuando se formó el W3C, encontraron mérito en ambos estilos de eventos y declararon que los navegadores deberían admitir ambos, a través de un tercer parámetro para el método addEventListener
. Originalmente, ese parámetro era solo un valor booleano, pero todos los navegadores modernos admiten un objeto options
como tercer parámetro, que puedes usar para especificar (entre otras cosas) si deseas usar la captura de eventos o no:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Ten en cuenta que el objeto options
es opcional, al igual que su propiedad capture
. Si se omite cualquiera de ellos, el valor predeterminado de capture
es false
, lo que significa que se usará el burbujeo de eventos.
Captura de eventos
¿Qué significa si tu controlador de eventos está "escuchando en la fase de captura"? Para entender esto, debemos saber cómo se originan los eventos y cómo se propagan. Lo siguiente es cierto para todos los eventos, incluso si tú, como desarrollador, no los aprovechas, no te importan o no los tienes en cuenta.
Todos los eventos comienzan en la ventana y primero pasan por la fase de captura. Esto significa que, cuando se envía un evento, inicia la ventana y se desplaza "hacia abajo" hacia su elemento de destino primero. Esto sucede incluso si solo escuchas en la fase de burbujeo. Considera el siguiente ejemplo de marcado y JavaScript:
<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,
);
Cuando un usuario hace clic en el elemento #C
, se envía un evento que se origina en window
. Luego, este evento se propagará a través de sus descendientes de la siguiente manera:
window
=> document
=> <html>
=> <body>
=> y así sucesivamente, hasta que alcance el objetivo.
No importa si no se está escuchando un evento de clic en el elemento window
, document
, <html>
o <body>
(o cualquier otro elemento en el camino hacia su destino). Un evento aún se origina en el window
y comienza su recorrido como se acaba de describir.
En nuestro ejemplo, el evento de clic se propaga (esta es una palabra importante, ya que se vinculará directamente con el funcionamiento del método stopPropagation()
y se explicará más adelante en este documento) desde el window
hasta su elemento de destino (en este caso, #C
) a través de cada elemento entre window
y #C
.
Esto significa que el evento de clic comenzará en window
y el navegador hará las siguientes preguntas:
"¿Hay algo que esté escuchando un evento de clic en el window
en la fase de captura?" Si es así, se activarán los controladores de eventos adecuados. En nuestro ejemplo, no se cumple ninguna, por lo que no se activará ningún controlador.
A continuación, el evento se propaga a document
y el navegador preguntará: "¿Hay algo que esté escuchando un evento de clic en document
en la fase de captura?". Si es así, se activarán los controladores de eventos adecuados.
A continuación, el evento se propaga al elemento <html>
y el navegador preguntará: "¿Hay algo que esté escuchando un clic en el elemento <html>
en la fase de captura?". Si es así, se activarán los controladores de eventos adecuados.
A continuación, el evento se propaga al elemento <body>
y el navegador preguntará: "¿Hay algo que esté escuchando un evento de clic en el elemento <body>
en la fase de captura?". Si es así, se activarán los controladores de eventos adecuados.
A continuación, el evento se propagará al elemento #A
. Una vez más, el navegador preguntará: "¿Hay algo que esté escuchando un evento de clic en #A
en la fase de captura? Si es así, se activarán los controladores de eventos adecuados".
A continuación, el evento se propagará al elemento #B
(y se hará la misma pregunta).
Por último, el evento llegará a su destino y el navegador preguntará: "¿Hay algo que esté escuchando un evento de clic en el elemento #C
en la fase de captura?". La respuesta esta vez es “sí”. Este breve período en el que el evento está en el destino se conoce como "fase de destino". En este punto, se activará el controlador de eventos, el navegador mostrará el mensaje "Se hizo clic en #C" en la consola y, luego, habremos terminado, ¿no?
Incorrecto. Aún no terminamos. El proceso continúa, pero ahora cambia a la fase de burbujeo.
Burbuja de eventos
El navegador te preguntará lo siguiente:
"¿Hay algo que esté escuchando un evento de clic en #C
en la fase de burbujeo?" Presta especial atención a este punto.
Es completamente posible detectar clics (o cualquier tipo de evento) en ambas fases: la de captura y la de propagación. Y si conectaste controladores de eventos en ambas fases (p.ej., llamando a .addEventListener()
dos veces, una con capture = true
y otra con capture = false
), entonces sí, ambos controladores de eventos se activarán para el mismo elemento. Sin embargo, también es importante tener en cuenta que se activan en diferentes fases (una en la fase de captura y otra en la fase de burbujeo).
A continuación, el evento se propaga (más comúnmente denominado "burbuja" porque parece que el evento viaja "hacia arriba" en el árbol del DOM) a su elemento superior, #B
, y el navegador preguntará: "¿Hay algo que esté escuchando eventos de clic en #B
en la fase de propagación?" En nuestro ejemplo, no se cumple ninguna, por lo que
no se activará ningún controlador.
A continuación, el evento se propagará a #A
y el navegador preguntará: "¿Hay algo que esté escuchando eventos de clic en #A
en la fase de propagación?".
A continuación, el evento se propagará a <body>
: "¿Hay algo que esté escuchando eventos de clic en el elemento <body>
en la fase de propagación?"
A continuación, el elemento <html>
: "¿Hay algo que esté escuchando eventos de clic en el elemento <html>
en la fase de burbujeo?
A continuación, el document
: "¿Hay algo que esté escuchando eventos de clic en el document
en la fase de burbujeo?"
Por último, window
: "¿Hay algo que esté escuchando eventos de clic en la ventana en la fase de burbujeo?"
¡Vaya! Fue un largo recorrido, y es probable que nuestro evento esté muy cansado en este momento, pero, lo creas o no, ese es el recorrido que atraviesa cada evento. La mayoría de las veces, esto nunca se nota porque, por lo general, a los desarrolladores solo les interesa una fase del evento o la otra (y, por lo general, es la fase de burbujeo).
Vale la pena dedicar un tiempo a experimentar con la captura y el burbujeo de eventos, y a registrar algunas notas en la consola a medida que se activan los controladores. Es muy útil ver la ruta que sigue un evento. Este es un ejemplo que escucha todos los elementos en ambas fases.
<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,
);
El resultado de la consola dependerá del elemento en el que hagas clic. Si hicieras clic en el elemento “más profundo” del árbol del DOM (el elemento #C
), verías que se activa cada uno de estos controladores de eventos. Con un poco de diseño CSS para que sea más obvio qué elemento es cuál, este es el elemento #C
de salida de la consola (también con una captura de pantalla):
"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"
Puedes interactuar con esta función en la demostración en vivo que aparece a continuación. Haz clic en el elemento #C
y observa el resultado de la consola.
event.stopPropagation()
Ahora que comprendemos dónde se originan los eventos y cómo se trasladan (es decir, se propagan) a través del DOM en la fase de captura y en la fase de activación, podemos enfocarnos en event.stopPropagation()
.
Se puede llamar al método stopPropagation()
en (la mayoría de) los eventos nativos del DOM. Digo "la mayoría" porque hay algunos en los que llamar a este método no hará nada (porque el evento no se propaga en primer lugar). Eventos como focus
, blur
, load
, scroll
y algunos otros pertenecen a esta categoría. Puedes llamar a stopPropagation()
, pero no sucederá nada interesante, ya que estos eventos no se propagan.
Pero ¿qué hace stopPropagation
?
Hace exactamente lo que dice. Cuando lo llames, el evento dejará de propagarse a los elementos a los que, de otro modo, se propagaría. Esto es cierto para ambos sentidos (captura y propagación). Por lo tanto, si llamas a stopPropagation()
en cualquier parte de la fase de captura, el
evento nunca llegará a la fase de destino ni a la fase de burbujeo. Si lo llamas en la fase de burbujeo, ya habrá pasado por la fase de captura, pero dejará de “subir” desde el punto en el que lo llamaste.
Volviendo a nuestro mismo ejemplo de marcado, ¿qué crees que sucedería si llamáramos a stopPropagation()
en la fase de captura en el elemento #B
?
El resultado sería el siguiente:
"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"
Puedes interactuar con esta función en la demostración en vivo que aparece a continuación. Haz clic en el elemento #C
de la demostración en vivo y observa el resultado de la consola.
¿Qué tal si detenemos la propagación en #A
en la fase de burbujeo? Esto generaría el siguiente resultado:
"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"
Puedes interactuar con esta función en la demostración en vivo que aparece a continuación. Haz clic en el elemento #C
de la demostración en vivo y observa el resultado de la consola.
Una más, solo por diversión. ¿Qué sucede si llamamos a stopPropagation()
en la fase de destino para #C
?
Recuerda que la "fase objetivo" es el nombre que se le da al período en el que el evento se encuentra en su objetivo. El resultado sería el siguiente:
"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"
Ten en cuenta que el controlador de eventos para #C
en el que registramos "hacer clic en #C en la fase de captura" aún se ejecuta, pero el que registramos "hacer clic en #C en la fase de burbujeo" no. Esto debería tener mucho sentido. Llamamos a stopPropagation()
desde el primero, por lo que ese es el punto en el que cesará la propagación del evento.
Puedes interactuar con esta función en la demostración en vivo que aparece a continuación. Haz clic en el elemento #C
de la demostración en vivo y observa el resultado de la consola.
Te recomiendo que experimentes con cualquiera de estas demostraciones en vivo. Intenta hacer clic solo en el elemento #A
o
solo en el elemento body
. Intenta predecir lo que sucederá y, luego, observa si tienes razón. En este punto, deberías poder hacer predicciones con bastante precisión.
event.stopImmediatePropagation()
¿Qué es este método extraño y poco usado? Es similar a stopPropagation
, pero en lugar de impedir que un evento se transmita a los descendientes (captura) o a los ancestros (burbuja), este método solo se aplica cuando tienes más de un controlador de eventos conectado a un solo elemento. Dado que addEventListener()
admite un estilo de eventos multicast, es completamente posible conectar un controlador de eventos a un solo elemento más de una vez. Cuando esto sucede (en la mayoría de los navegadores), los controladores de eventos se ejecutan en el orden en que se conectaron. Llamar a stopImmediatePropagation()
evita que se activen los controladores posteriores. Consulta el siguiente ejemplo:
<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,
);
El ejemplo anterior generará el siguiente resultado de la consola:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Ten en cuenta que el tercer controlador de eventos nunca se ejecuta debido a que el segundo llama a e.stopImmediatePropagation()
. Si, en cambio, hubiéramos llamado a e.stopPropagation()
, el tercer controlador
se ejecutaría de todos modos.
event.preventDefault()
Si stopPropagation()
evita que un evento se transmita "hacia abajo" (captura) o "hacia arriba" (burbuja), ¿qué hace preventDefault()
? Parece que hace algo similar. ¿Es así?
En realidad, no. Si bien a menudo se confunden, en realidad no tienen mucho que ver entre sí.
Cuando veas preventDefault()
, agrega en tu mente la palabra “acción”. Piensa en “evitar la acción predeterminada”.
¿Cuál es la acción predeterminada que podrías solicitar? Lamentablemente, la respuesta a esa pregunta no es tan clara porque depende en gran medida de la combinación de elementos y eventos en cuestión. Y, para complicar aún más las cosas, a veces no hay ninguna acción predeterminada.
Comencemos con un ejemplo muy simple para comprenderlo. ¿Qué esperas que suceda cuando haces clic en un
vínculo en una página web? Por supuesto, esperas que el navegador navegue a la URL especificada por ese vínculo.
En este caso, el elemento es una etiqueta de ancla y el evento es un evento de clic. Esa combinación (<a>
+ click
) tiene una "acción predeterminada" de navegar al href del vínculo. ¿Qué sucede si quieres evitar que el navegador realice esa acción predeterminada? Es decir, supongamos que quieres evitar que el navegador navegue a la URL especificada por el atributo href
del elemento <a>
. Esto es lo que hará preventDefault()
por ti. Considera el siguiente ejemplo:
<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,
);
Puedes interactuar con esta función en la demostración en vivo que aparece a continuación. Haz clic en el vínculo The Avett Brothers y observa el resultado de la consola (y el hecho de que no se te redirecciona al sitio web de Avett Brothers).
Por lo general, si haces clic en el vínculo etiquetado como The Avett Brothers, se te redireccionará a www.theavettbrothers.com
. Sin embargo, en este caso, conectamos un controlador de eventos de clic al elemento <a>
y especificamos que se debe evitar la acción predeterminada. Por lo tanto, cuando un usuario haga clic en este vínculo, no se navegará a ningún lugar y, en su lugar, la consola simplemente registrará "¿Quizás deberíamos reproducir parte de su música aquí?".
¿Qué otras combinaciones de elementos o eventos te permiten evitar la acción predeterminada? No puedo mencionarlas todas, y a veces solo tienes que experimentar para ver. Sin embargo, aquí tienes algunos ejemplos:
Elemento
<form>
+ evento "submit":preventDefault()
para esta combinación evitará que se envíe un formulario. Esto es útil si deseas realizar una validación y, si algo falla, puedes llamar de forma condicional a preventDefault para evitar que se envíe el formulario.Elemento
<a>
+ evento "click":preventDefault()
para esta combinación evita que el navegador navegue a la URL especificada en el atributo href del elemento<a>
.document
+ evento "mousewheel":preventDefault()
para esta combinación evita que la página se desplace con la rueda del mouse (aunque el desplazamiento con el teclado seguirá funcionando).
↜ Esto requiere llamar aaddEventListener()
con{ passive: false }
.document
+ evento "keydown":preventDefault()
para esta combinación es letal. Esto hace que la página sea prácticamente inútil, ya que impide el desplazamiento del teclado, el uso de la tecla Tab y el uso de la función de destacar del teclado.document
+ evento "mousedown":preventDefault()
para esta combinación evitará que el texto se destaque con el mouse y cualquier otra acción "predeterminada" que se invoque con el mouse presionado.Elemento
<input>
+ evento "keypress":preventDefault()
para esta combinación evitará que los caracteres que escribe el usuario lleguen al elemento de entrada (pero no lo hagas; rara vez, o nunca, hay un motivo válido para hacerlo).document
+ evento "contextmenu":preventDefault()
para esta combinación evita que aparezca el menú contextual nativo del navegador cuando un usuario hace clic con el botón derecho o mantiene presionado (o cualquier otra forma en que pueda aparecer un menú contextual).
Esta lista no es exhaustiva, pero esperamos que te brinde una buena idea de cómo se puede usar preventDefault()
.
¿Una broma divertida?
¿Qué sucede si stopPropagation()
y preventDefault()
en la fase de captura, comenzando en el documento? ¡Se desata la hilaridad! El siguiente fragmento de código renderizará cualquier página web de manera que sea prácticamente inútil:
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 });
No sé por qué querrías hacer esto (excepto para gastarle una broma a alguien), pero es útil pensar en lo que sucede y comprender por qué se crea la situación que se crea.
Todos los eventos se originan en window
, por lo que, en este fragmento, detenemos todos los eventos click
, keydown
, mousedown
, contextmenu
y mousewheel
para que nunca lleguen a ningún elemento que pueda escucharlos. También llamamos a stopImmediatePropagation
para que se frustren todos los
controladores conectados al documento después de este.
Ten en cuenta que stopPropagation()
y stopImmediatePropagation()
no son (al menos, no en su mayoría) lo que hace que la página sea inútil. Simplemente evitan que los eventos lleguen a donde irían de otra manera.
Pero también llamamos a preventDefault()
, que, como recordarás, evita la acción predeterminada. Por lo tanto, se impiden todas las acciones predeterminadas (como el desplazamiento de la rueda del mouse, el desplazamiento del teclado o el resaltado o la tabulación, hacer clic en un vínculo, mostrar el menú contextual, etcétera), lo que deja la página en un estado bastante inútil.
Demostraciones en vivo
Para volver a explorar todos los ejemplos de este artículo en un solo lugar, consulta la demostración incorporada a continuación.
Agradecimientos
Imagen hero de Tom Wilson en Unsplash.