Custom Events en componentes

Comunicar componentes con eventos personalizados


Si sabes 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 propios eventos personalizados 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

Recordemos que los Custom Events nos permiten crear nuestros propios eventos personalizados para emitir información a lugares externos. Observa el siguiente fragmento de código:

// Creamos el evento
const event = new CustomEvent("user:message-received", { detail: { name: "Manz" }});

// Emitimos el evento al elemento indicado
const element = document.querySelector(".element");
element.dispatchEvent(event);

Si no has comprendido bien este fragmento de código, o quieres saber más detalles sobre ello, antes de continuar te aconsejo echar un vistazo a este artículo donde explico la base de los Eventos personalizados (Custom Events):

Aprender más sobre Eventos personalizados en Javascript

¿Por qué Custom Events?

Aunque el objetivo principal de los componentes es aislar todo dentro de un componente de modo que sea modular y reutilizable, van a existir situaciones en las que el componente tiene que comunicarse con elementos de su exterior, tanto para enviar información como para recibirla.

Sin embargo, debemos hacerlo de una forma en la que no indiquemos expresamente el elemento con el que nos comunicaríamos, ya que eso sería una mala práctica dentro del mundo de la programación: si en el futuro ese elemento al que hacemos referencia cambia, tendremos que cambiar todos los que dependan de él, lo que puede ser una tarea agotadora, poco práctica y muy tediosa.

Por lo tanto, la mejor forma de hacer esta comunicación, es utilizando eventos y emitiéndolos fuera del componente. De esta forma, otros componente pueden recibir esos eventos o información y tratarlos. Y ocurre lo mismo con la recepción de información.

Composed: Atravesar Shadow

Antes de continuar, un detalle importante cuando utilizamos Custom Events en WebComponents. Por defecto, cuando emitimos un evento, este no atraviesa el Shadow DOM, ya que recordemos que la finalidad principal del Shadow DOM es encapsular y aislar. Por lo tanto, 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. De esta forma, al emitirlo, tiene la orden expresa de atravesar el Shadow DOM y continuar su propagación (si tiene bubbles a true).

Emitir eventos hacia el exterior

Si ya conocemos los custom events y el método mágico handleEvent, vamos a aplicarlos a un WebComponent para emitir información desde el componente hacia su exterior:

class FirstElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

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

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`<button>Send message!</button>`;
    this.shadowRoot.querySelector("button").addEventListener("click", this);
  }
};
customElements.define("first-element", FirstElement);
<first-element></first-element>

Detalles clave de este fragmento de código:

  1. El evento personalizado tiene el flag composed a true, por lo que atravesará el Shadow DOM.
  2. El evento personalizado tiene el flag bubbles a true, por lo que burbujeará hacia arriba en el DOM.
  3. Se hace uso del método mágico handleEvent para procesar los eventos.

Cuando el usuario hace click en el botón que tiene el componente con el texto Send message!, se dispara un evento nativo click y se procesa por el método handleEvent(). En este método, se crea un evento personalizado que se envía al DOM por medio de this en el .dispatchEvent(), por lo que se enviará a la propia etiqueta del componente, es decir, al elemento <first-element>.

Como se activa el flag bubbles, el evento se propagará a sus elementos padres y el flag composed permite que atraviese los Shadow DOM en el caso de encontrarlos. Así pues, el evento personalizado se propagará al elemento HTML contenedor de <first-element> y continuará propagándose hasta llegar a document, que es el elemento padre superior.

Recibir eventos desde el exterior

En el siguiente fragmento de código, veremos dos componentes: <first-element>, que es el componente del ejemplo anterior, y <second-element> que es el segundo componente, que va a encargarse de recibir el evento enviado por el primero.

Veamos como realizar una implementación simple:

class SecondElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.data = null;
  }

  handleEvent(event) {
    if (event.type === "user:data-message") {
      this.data = event.detail;
      this.render();
    }
  }

  connectedCallback() {
    document.addEventListener("user:data-message", this);
    this.render();
  }

  noMessages() {
    return /* html */`<div>No messages</div>`;
  }

  hasMessages() {
    return /* html */`<div class="container">
      From ${this.data.from}:
      <span style="color:red">${this.data.message}</span>
    </div>`;
  }

  render() {
    this.shadowRoot.innerHTML = this.data ? this.hasMessages() : this.noMessages();
  }
};
customElements.define("second-element", SecondElement);
<first-element></first-element>
<second-element></second-element>

Comentemos los detalles más relevantes de este fragmento de código:

  1. En primer lugar, observa que una propiedad de clase this.data se inicializa a null. En ella se guardará la información recibida por el evento para tenerla disponible para toda la clase.

  2. Hemos creado un método de clase render() que es al que se va a llamar cuando necesitemos pintar algo en el navegador. Se llamará al iniciar el componente (en el connectedCallback()) y cada vez que reciba datos desde el exterior (en el handleEvent()).

  3. El método render() consulta la propiedad this.data mediante un operador ternario, si no tiene información, muestra el contenido HTML del método noMessages(), en caso de tener información, muestra el contenido HTML del método hasMessages().

  4. Observa que en el connectedCallback() escuchamos el evento en document, es decir, en el elemento raíz del documento HTML. De esta forma, si el otro componente tiene el bubbles y el composed activado, podremos leer el evento sin problemas, e implementar la funcionalidad correspondiente en este componente.

¿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