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.
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?.
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.
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.
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:
sendMessage
la hemos colocado dentro del componente, como un método de clase..innerHTML
que hacíamos antes, solo insertamos el marcado HTML del botón, sin evento.this.button
guardará una referencia al botón que añadimos en el Shadow DOM.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.
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 comoclick
omousedown
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.
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.
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