Propagación de eventos

Como gestionar y manejar la propagación de eventos


Ahora que conocemos lo básico sobre los eventos nativos y los eventos personalizados, así como la emisión y propagación de eventos que afecta a ambos, vamos a aprender un poco más sobre esta propagación, como gestionarla, prevenirla y modificarla si es necesario.

En primer lugar, recordemos como gestionamos un evento y su propagación:

<div class="root">
  <div class="parent">
    <div class="child">
      <button>Click me!</button>
    </div>
  </div>
</div>

Tenemos esta estructura HTML, donde existe un botón dentro de tres elementos <div> anidados uno dentro del otro en el DOM. Vamos a escuchar los eventos de tipo click en el <button> y cuando ocurra alguno, crearemos un evento personalizado y lo enviaremos al botón. Como tenemos el bubbles activado, no se detendrá, sino que seguirá propagándose hacia sus contenedores padres:

const button = document.querySelector("button");
const root = document.querySelector(".root");

button.addEventListener("click", (event) => {
  const customEvent = new CustomEvent("warning", { bubbles: true });
  button.dispatchEvent(customEvent);
});

root.addEventListener("warning", (event) => {
  console.log("Evento click recibido en el root.", event);
});

Observa que al final nos hemos puesto a escuchar los eventos ocurridos en el elemento <div> raíz, es decir, el primero de todos. Si llega hasta ahí nuestro evento personalizado, ejecutará el console.log() mostrando dicho evento. De no tener el bubbles a true, el custom event nunca se habría propagado hasta al elemento root, sino que se habría quedado en el el elemento <button>.

Propagación de eventos

Vamos a analizar como funciona la propagación de eventos en Javascript. Los eventos tienen una serie de propiedades que analizaremos a continuación. Son las siguientes:

Propiedad o Método Descripción
Propiedades
.bubbles Indica si el evento se propagará hacia contenedores padres o se detendrá en el elemento emitido.
.composed Indica si el evento puede atravesar un Shadow DOM en su propagación, o no.
Destino del evento
.target Indica el elemento objetivo (donde se hizo el dispatchEvent()).
.currentTarget Indica el elemento actual donde se ha escuchado el evento.
Método
.composedPath() Muestra el camino de elementos por donde se ha propagado el evento.

Centremonos en el fragmento de código anterior, donde creamos el evento personalizado y establecemos si se va a propagar el evento. En el caso anterior, hemos activado la propiedad bubbles del segundo parámetro de opciones de la instancia del evento personalizado con new CustomEvent():

const customEvent = new CustomEvent("warning", { bubbles: true });
button.dispatchEvent(customEvent);

Propagación y Shadow DOM

Hay que tener en cuenta que pueden ocurrir varias cosas:

  1. Si el flag bubbles está desactivado, el evento se emite a <button> y se detiene ahí.
  2. Si el flag bubbles está activado, el evento se emite a <button>, luego a su contenedor padre, y así sucesivamente.
  3. Si el flag composed está desactivado, el evento se detendrá al encontrar un Shadow DOM.
  4. Si el flag composed está activado, el evento no se detendrá si encuentra un Shadow DOM.

Trayectoria de propagación

Si tienes dudas, una buena forma de comprobar el camino que ha seguido el evento emitido con bubbles es ejecutando el método .composedPath(). Este nos mostrará por donde ha ido pasando el evento:

root.addEventListener("warning", (event) => {
  console.log("Evento click recibido en el root.");
  const path = event.composedPath();
  console.log(path);
});

// path = [button, div.child, div.parent, div.root, body, html, document, Window]

Observa que la constante path tiene un con los elementos por donde ha ido pasando. En primer lugar, el evento fue emitido al <button>, luego a su padre .child, luego a su padre .parent, luego a su padre .root, luego al <body>, luego al <html> y por último al document (el documento actual) y Window la pestaña actual del navegador.

Por otro lado, la propiedad .target nos dará el elemento desde donde se emitió el evento, <button> en nuestro caso, y la propiedad .currentTarget nos devolverá el elemento actual en el que se encuentra, .root en nuestro caso.

Detener la propagación

Por defecto, los eventos nativos tienen la propiedad .cancelable a true. Esto significa que los eventos pueden cancelar su propagación utilizando los métodos .stopPropagation() o .stopImmediatePropagation().

Propiedad o Método Valor por defecto Descripción
Propiedad
.cancelable true Indica si es posible cancelar el evento.
Métodos
.stopPropagation() Detiene la propagación en el evento en cuestión.
.stopImmediatePropagation() Detiene la propagación en todos los eventos del mismo tipo.

Vamos a modificar el ejemplo inicial que teníamos, y añadir un evento en el elemento intermedio .parent que cancele la propagación de eventos mediante .stopPropagation(). Esta detención de la propagación de eventos en .parent debería evitar que el evento llegue hasta .root, a pesar de tener el flag bubbles activo.

const root = document.querySelector(".root");
root.addEventListener("warning", (event) => {
  console.log("Evento click recibido en el root.", event);
});

const parent = document.querySelector(".parent");
parent.addEventListener("warning", (event) => {
  parent.stopPropagation();
  console.log("Recibido en parent");
});

La diferencia de .stopPropagation() y .stopImmediatePropagation() es que este último detiene la propagación en todos los eventos de su mismo tipo, mientras que el primero sólo detiene el evento concreto donde lo escribimos. Recuerda que para que estos métodos funcionen, el evento debe tener el flag .cancelable a true.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev