Custom Events en WebComponents

Si conoces como funcionan los eventos en WebComponents (o en Javascript en general), te resultarán muy interesantes los custom events, un mecanismo derivado que nos permite crear nuestros eventos personalizados propios y así utilizarlos en momentos concretos de nuestro código, para disparar funciones asociadas al igual que se hace con los eventos habituales en Javascript, como click o input, por ejemplo.

Custom Events: WebComponents

Además, los custom events (eventos personalizados) cobran especial importancia en materia de WebComponents, ya que también se suelen utilizar para comunicar entre componentes y hacerlo de una forma en la que mantengamos la lógica en los componentes que deben tenerla, sin acoplar código a componentes ajenos o generar dependencias externas.

Custom Events

Crear un evento personalizado es muy sencillo. Se basa en crear una instancia del objeto CustomEvent, al cuál le pasaremos un con el nombre que le hemos puesto a nuestro evento, y un segundo parámetro que será un de opciones, que veremos más adelante.

const MessageEvent = new CustomEvent("message", options);

En lugar de CustomEvent también se puede indicar simplemente Event (o alguno de sus objetos derivados). La diferencia radica en que CustomEvent se suele utilizar cuando queremos añadir datos personalizados, como vamos a hacer a continuación en las opciones.

Nota: Una buena práctica es comenzar eligiendo un buen de nombre de evento, que sea bastante «autoexplicativo» en cuanto la acción que vamos a realizar.

Opciones de Custom Events

El segundo parámetro del CustomEvent es un donde podremos especificar varios detalles en relación al comportamiento o contenido del evento. A continuación, tienes una lista de las propiedades que pueden contener estas opciones:

Opciones Valor Descripción
detail false Objeto que contiene toda la información que queremos transmitir.
bubbles false Indica si el evento debe burbujear en el DOM «hacia la superficie» o no.
composed false Indica si la propagación puede atravesar Shadow DOM o no.
cancelable false Indica si el comportamiento se puede cancelar con .preventDefault().

Por ejemplo, en el siguiente fragmento de código vemos como declarar una instancia de un CustomEvent llamado message, el cuál tiene ciertas opciones definidas, entre las que se encuentran que burbujee hacia arriba en el DOM y que pueda atravesar Shadow DOM (útil cuando se trata de componentes). Además, contiene un objeto con información personalizada en detail:

const MessageEvent = new CustomEvent("message", {
  detail: {
    from: "Manz",
    message: "Hello!"
  },
  bubbles: true,
  composed: true
});

Dentro del objeto de opciones, podemos ver que tenemos un objeto detail que es definido por el desarrollador, ya que es el diseñador del evento personalizado. Pero quizás, lo que más confusión trae siempre es el tema de los flags bubbles y composed, así que vamos a explicarlos.

Emitir eventos

A la hora de emitir un evento, en Javascript lo que hacemos es crear el evento y enviarlo a un elemento del DOM. Dependiendo de las opciones de ese evento, el evento terminará ahí o se propagará hacia sus elementos padres del DOM sucesivamente hasta llegar al primero. Esto último es lo que se conoce como burbujear (bubbles).

Para emitir eventos se utiliza el método .dispatchEvent(), al cuál le pasamos el evento personalizado que deseamos emitir. Este método se lanza sobre el elemento del DOM implicado, y de tener la opción bubbles a true, comenzará a emitirse sucesivamente a través de sus contenedores padres.

Para verlo más claro, imaginemos un fragmento de código donde tenemos tres elementos <div> anidados, es decir, uno dentro de otro. El primer <div> lo numeramos con un 1 (rojo), el segundo con un 2 (azul), mientras que el <div> más profundo, el que está en el interior de todos ellos, lo numeramos con un 3 (lila):

Events: Bubbles, capture and composed

En la parte de Javascript, observa que trabajaremos sobre el elemento con clase box-3. Lo primero que hacemos es prepararnos para un evento click. Si el usuario hace click en este <div> lila, emitiremos un evento personalizado message con información al elemento box-3, ya que es donde estamos haciendo el .dispatchEvent():

<div class="box-1">
  <div class="box-2">
    <div class="box-3"></div>
  </div>
</div>

<script>
  const box3 = document.querySelector(".box-3");

  box3.addEventListener("click", () => {
    box3.dispatchEvent(new CustomEvent("message", { detail: { name: "Manz" } }));
  });

  box3.addEventListener("message", () => console.log("Message received!"));
</script>

Por otro lado, también nos preparamos para recibir este evento con un addEventListener(). Este ejemplo no tiene demasiado sentido, ya que es sólo un ejemplo teórico, sin utilidad práctica. Pero vamos a complicarlo un poco para hacerlo más interesante.

Propagación de eventos (bubbles)

En primer lugar, vamos añadir unos atributos data-num en el marcado HTML. Esto no es más que unos metadatos de marcado HTML que podremos recoger desde Javascript más tarde. En segundo lugar, observa que a la hora de emitir el evento, hemos establecido el flag bubbles a true, que por defecto está a false.

En tercer lugar, la diferencia principal es que ahora emitimos el evento desde el elemento box-3, pero escuchamos en el elemento box-1, que es, por decirlo así, su «abuelo»:

<div class="box-1" data-num="1">
  <div class="box-2" data-num="2">
    <div class="box-3" data-num="3"></div>
  </div>
</div>

<script>
  const box1 = document.querySelector(".box-1");
  const box3 = document.querySelector(".box-3");

  box3.addEventListener("click", () => {
    box3.dispatchEvent(new CustomEvent("message", {
      bubbles: true,
      detail: {
        name: "Manz"
      }
    }));
  });

  box1.addEventListener("message", (event) => {
    const name = event.detail.name;
    const number = event.target.dataset.num;
    console.log(`Message received from ${name} (${number})`);
  });
</script>

Al establecer bubbles a true, en lugar de emitir el evento y detenerse en ese primer elemento indicado, el evento se irá propagando por sus padres, hasta llegar al elemento padre del documento.

Observa también, que hemos utilizado como parámetro de la función event, lo que nos va a dar información sobre el evento. Por ejemplo, mediante event.target podemos acceder al elemento al que se ha emitido originalmente el evento, mientras que con event.detail podemos acceder a la información que se incluyó en el evento al crearlo.

Recepción de eventos (capture)

Si registraramos como se emiten los eventos del ejemplo anterior, con un .addEventListener() en cada uno de los elementos <div>, observaríamos que el orden de recepción de eventos es 3, 2, 1, es decir, primero se disparan los eventos en los elementos interiores, y luego en los elementos padres a medida que se burbujea hacia arriba.

Este es el comportamiento por defecto de la fase de burbujeo de Javascript, sin embargo, podemos activar la fase de captura, que invierte el orden. Para ello, solo tenemos que modificar el ejemplo anterior, añadiendo un tercer parámetro en el .addEventListener() que será un objeto de opciones que contendrá capture a true:

box1.addEventListener("message", (event) => {
  const name = event.detail.name;
  const number = event.target.dataset.num;
  console.log(`Message received from ${name} (${number})`);
}, { capture: true });

De esta forma, Javascript no procesa inmediatamente la función asociada al evento disparado, sino que los va capturando y los ejecuta en la vuelta, de modo que en este caso, el orden termina siendo 1, 2, 3.

Composed: Atravesar Shadow

En la zona inferior de la imagen anterior, hay una pequeña modificación sobre el ejemplo que hemos tratado hasta ahora. El elemento <div> número 2, lo vamos a cambiar por un WebComponent con Shadow DOM.

Por defecto, cuando emitimos un evento, este no atraviesa el Shadow DOM, por lo que si hacemos un .dispatchEvent() desde el interior de un WebComponent, este no llegará a salir del mismo. Si queremos que atraviese el Shadow DOM del componente, deberemos indicar el flag composed a true a la hora de crear el evento.

A continuación, podemos ver un ejemplo donde comunicamos dos WebComponents a través de eventos personalizados:

<first-element></first-element>
<second-element></second-element>

<script>
/** FirstElement component **/
customElements.define("first-element", class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  handleEvent(event) {
    if (event.type === "click") {
      const MessageEvent = new CustomEvent("message", {
        detail: { from: "Manz", message: "Hello!" },
        bubbles: true,
        composed: true
      });
      this.dispatchEvent(MessageEvent);
    }
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<button>Send message!</button>`;
    this.shadowRoot.querySelector("button").addEventListener("click", this);
  }
});

/** SecondElement component **/
customElements.define("second-element", class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  handleEvent(event) {
    if (event.type === "message") {
      event.detail.from = "Robot";
      const data = event.detail;
      this.shadowRoot.innerHTML = `
        <div>
          From ${data.from}:
          <span style="color:red">${data.message}</span>
        </div>
      `;
    }
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<div>No messages</button>`;
    document.addEventListener("message", this);
  }
});
</script>

Nota: Observa que en este ejemplo hemos utilizado algunos de los detalles mencionados en capítulos anteriores, como por ejemplo el uso de la función mágica handleEvent(), explicada en eventos en WebComponents.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Autor de Emezeta.com, es profesor en la Universidad de La Laguna y dirige el curso de Programación web FullStack y Diseño web FrontEnd de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.