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, en Javascript se suele utilizar la última forma, pero si no conoces como funcionan los eventos te recomiendo que antes de continuar eches un vistazo al 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.

Usando eventos HTML

Empecemos con la primera forma, gestionando eventos a través de atributos HTML:

<app-element></app-element>

<script>
const sendMessage = () => alert("¡Has hecho click!");

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

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<button onClick="sendMessage()">👀 Press me!</button>
`
;
}
};
customElements.define("app-element", AppElement);
</script>

En este ejemplo vemos un componente muy sencillo con el evento aplicado. 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.

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.

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:

<app-element></app-element>

<script>
class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

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

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

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

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

  • La función sendMessage la hemos colocado dentro del componente, como 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 que añadimos en el Shadow 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:

<app-element></app-element>

<script>
class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

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

connectedCallback() {
this.shadowRoot.innerHTML = "<button>👀 Press me!</button>";
this.button = this.shadowRoot.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);
</script>

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 lo mismo), sino que tendríamos que guardar la anterior en una propiedad de clase, lo que probablemente iría complicando demasiado la lógica y puede que a la larga, no nos interese.

Nota: Hay que tener mucho cuidado con los eventos cuando reescribimos parte del DOM de nuestro componente, especialmente cuando utilizamos .innerHTML. Eventos como click o mousedown están asociados a los elementos del DOM, por lo que si los reescribimos perderemos el evento, que ya no estará escuchando en el nuevo elemento del DOM, como quizás pensemos.

El método mágico 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:

<app-element></app-element>

<script>
class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

handleEvent(event) {
if (event.type === "click")
this.sendMessage();
}

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

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

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

Observa que en el segundo parámetro del método .addEventListener() simplemente colocamos this (una referencia al objeto instanciado a partir de la clase). 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.

Tabla de contenidos