Eventos en WebComponents

Como manejar eventos en WebComponents


Al igual que en cualquier documento, al crear WebComponents nos interesará prepararnos ante ciertas acciones interactivas que el usuario podría realizar, por lo que sería bueno echar un vistazo a las posibilidades que tenemos y las recomendaciones que existen al utilizar eventos en componentes nativos.

WebComponents: Events

¿Qué son los eventos?

Los eventos son acciones que realiza el usuario (consciente o inconscientemente), y que como desarrolladores, podemos prever y preparar en el código para saber manejarlos y decirle a nuestra página o aplicación como debe actuar cuando ocurra un evento específico.

Existen tres formas principales de definir y utilizar eventos en nuestro código:

Forma Ejemplo Artículo en profundidad
Mediante atributos HTML <button onClick="..."></button> Eventos JS desde atributos HTML
Mediante propiedades Javascript .onclick = function() { ... } Eventos JS desde propiedades Javascript
Mediante addEventListener() .addEventListener("click", ...) Eventos JS desde listeners

En general, lo recomendable es utilizar la última forma, ya que es la más potente y reutilizable, pero si no conoces como funcionan los eventos te recomiendo que antes de continuar leas el artículo ¿Qué son los eventos en Javascript?.

Eventos en componentes

Dicho esto, vamos a ver como podríamos utilizar eventos en componentes, analizando las tres estrategias anteriores, aplicándolo a lo que hemos aprendido de WebComponents, y observando las limitaciones y características de cada estrategia:

  • 1️⃣ Eventos mediante atributos HTML
  • 2️⃣ Eventos mediante propiedades Javascript / bind
  • 3️⃣ Eventos mediante listeners
  • 4️⃣ Eventos mediante método mágico handleEvent

Usando eventos mediante HTML

Empecemos con la primera forma. En este ejemplo vemos un componente muy sencillo con el evento aplicado, gestionando eventos a través de atributos HTML:

window.sendMessage = () => alert("¡Has hecho click!");

class AppElement extends HTMLElement {
  connectedCallback() {
    this.innerHTML = /* html */`
      <button onClick="sendMessage()">👀 Press me!</button>
    `;
  }
};

customElements.define("app-element", AppElement);
<app-element></app-element>

Sin embargo, hay un problema bastante evidente: la función sendMessage() asociada debe estar en un contexto global, lo que en nuestro ejemplo se traduce como poner la función fuera de la clase del componente y fijarla en window.

Esto no nos interesa, ya que precisamente lo que buscamos es modularizar toda la funcionalidad relacionada en el propio componente, evitando "polucionar" el contexto global y que el componente no dependa de esa función.

Usando addEventListener / bind

Vamos a intentar mejorar un poco nuestro ejemplo, utilizando el método addEventListener para eliminar las limitaciones que tuvimos en el ejemplo anterior.

Observa que al igual que añadimos el listener en el connectedCallback(), es una buena práctica eliminarlos en el disconnectedCallback(). Esto puede ser necesario o prescindible, dependiendo del uso que se le dé al componente y si puede llegar a ser eliminado de la página:

class AppElement extends HTMLElement {
  sendMessage() {
    alert("¡Has hecho click!");
  }

  connectedCallback() {
    this.innerHTML = "<button>👀 Press me!</button>";
    this.button = this.querySelector("button");
    this.button.addEventListener("click", this.sendMessage.bind(this));
  }

  disconnectedCallback() {
    this.button.removeEventListener("click", this.sendMessage.bind(this));
  }
};

customElements.define("app-element", AppElement);
<app-element></app-element>

La cosa se empieza a complicar. Repasemos que hemos hecho:

  • La función sendMessage la hemos colocado dentro del componente, en un método de clase.
  • Ahora, en el .innerHTML que hacíamos antes, solo insertamos el marcado HTML del botón, sin evento.
  • La propiedad de clase this.button guardará una referencia al botón del DOM.
  • Añadimos un evento de tipo click que lance el método de clase sendMessage().

Quizás, lo más extraño del ejemplo anterior tenga relación con el último punto. Si quieres profundizar en las diferentes formas de escuchar y manejar eventos y ese extraño .bind(), echa un vistazo al artículo Escuchar y manejar eventos Javascript.

Usando addEventListener / arrow

Si no nos convence la forma del apartado anterior, podemos utilizar funciones flecha anónimas, donde nuestro componente quedaría como se puede ver a continuación:

class AppElement extends HTMLElement {
  sendMessage() {
    alert("¡Has hecho click!");
  }

  connectedCallback() {
    this.innerHTML = "<button>👀 Press me!</button>";
    this.button = this.querySelector("button");
    this.button.addEventListener("click", () => this.sendMessage());
  }

  disconnectedCallback() {
    /* ❌ Ojo, esto no funcionará correctamente */
    this.button.removeEventListener("click", () => this.sendMessage());
  }
};

customElements.define("app-element", AppElement);
<app-element></app-element>

Observa que el problema de utilizar una función flecha anónima en el .addEventListener(), es que complica la posibilidad de poder realizar un .removeEventListener(), ya que este requiere la misma función que se utilizó al realizar el .addEventListener().

No nos sirve volver a definir una nueva función flecha anónima igual a la anterior (seguiría siendo una función diferente aunque haga exactamente lo mismo). En su lugar, deberíamos guardar la función anterior en una propiedad de clase, lo que probablemente complicaría la lógica y puede que a la larga, no nos interese.

Hay que tener mucho cuidado con los eventos al reescribir parte del DOM de nuestro componente, especialmente cuando utilizamos .innerHTML. Eventos como click están asociados a elementos del DOM. Si los sobreescribimos, el nuevo elemento del DOM ya no tendrá evento escuchando, y peor aún, el anterior podría seguir funcionando y consumiendo recursos, aunque se vuelva a disparar.

Usando el método handleEvent

Existe una opción más para gestionar los eventos, que se basa en la función mágica handleEvent. Con esta forma solucionaríamos todos los problemas mencionados, y permite organizar un poco mejor el código, de forma que sea más mantenible.

Veamos a continuación como adaptarla a un WebComponent:

class AppElement extends HTMLElement {
  handleEvent(event) {
    if (event.type === "click")
      this.sendMessage();
  }

  sendMessage() {
    alert("¡Has hecho click!");
  }

  connectedCallback() {
    this.innerHTML = "<button>👀 Press me!</button>";
    this.button = this.querySelector("button");
    this.button.addEventListener("click", this);
  }

  disconnectedCallback() {
    this.button.removeEventListener("click", this);
  }
};
customElements.define("app-element", AppElement);
<app-element></app-element>

Observa que en el segundo parámetro del método .addEventListener() simplemente colocamos un this (una referencia al propio componente en cuestión). Como es una referencia al objeto, el navegador buscará mágicamente si existe un método llamado .handleEvent() y si existe, procesa el evento con él.

En dicho método, podemos comprobar el event.type (tipo de evento lanzado: click, mousedown, mousemove, etc...) y ejecutar la función que buscamos. De esta forma centralizamos en este método la gestión de las funciones necesarias, por lo que nos quedará todo mucho más organizado.

Existen algunas librerías externas (de terceros) como lit-html o htm que facilitan la forma en la que trabajamos con el HTML, DOM y eventos en componentes, y pueden hacernos la vida un poco más fácil. Las veremos un poco más adelante.

¿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