Interoperabilidad entre componentes. Cómo realizar la transmisión de datos de hijos a padres u otros ancestros disparando y recibiendo eventos personalizados.
En el Manual de Lit hemos explicado ya las bases de la interoperabilidad de componentes. Vimos que existe un modo de trabajo habitual, consistente en el bindeo de propiedades para las comunicaciones de padres a hijos y el uso de eventos para las comunicaciones desde los hijos a los pares.
Para ilustrar todas las situaciones de interoperabilidad hemos empezado con el sistema de binding y ahora nos vamos a dedicar a conocer las comunicaciones mediante eventos, que son la vía normal para enviar datos de los hijos a los padres y abuelos.
Eventos personalizados
El envío de eventos se apoya totalmente en las características ofrecidas por el lenguaje Javascript. Para ello usamos las funciones del API del DOM necesarias para instanciar y despachar los eventos.
En Javascript tenemos eventos estándar que son disparados por los elementos HTML, pero en el caso de los componentes generalmente usaremos eventos personalizados. Mediante eventos personalizados podemos avisar de cualquier situación que ocurra en los componentes y la señal del evento se podrá recibir en el componente padre, pero también en cualquier otro componente que forme parte de la rama ascendente en el árbol DOM en la que nos encontremos, llegando finalmente el evento al objeto document y luego al window.
La gestión de eventos personalizados en Javascript ya ha sido materia de estudio en otros artículos, así que te recomendamos que aprendas a manejarlos antes de continuar con la lectura de este artículo: entender los eventos personalizados de Javascript.
Nuestros componentes podrán lanzar eventos de todo tipo, totalmente adaptados a sus necesidades. Por ejemplo un componente de selección de posibles valores en una lista podrá enviar un evento personalizado cuando se seleccione uno de esos valores. Un componente de selección de fecha podría lanzar un evento personalizado cuando se seleccione una fecha. Los ejemplos son infinitos.
Componente de cuenta atrás para lanzar eventos personalizados al acabar
Vamos a hacer un sencillo componente de cuenta atrás. Este componente debe contar un número de segundos de manera descendente y, cuando la cuenta atrás acabe, debe enviar un evento personalizado al padre, que deberá capturarlo. Al componente de cuenta atrás le da lo mismo quién lo use y qué se tenga que hacer cuando se ha terminado el tiempo, simplemente avisará que ha llegado al fin y quien quiera que use ese componente establecerá qué comportamiento se implementará.
Por poner algunas ideas sobre en qué podríamos usar la cuenta atrás vamos a mencionar: realizar el auto-save de un texto, realizar una sincronización con el servidor, cerrar una caja de diálogo… es indiferente. Lo interesante es que el componente de cuenta atrás será capaz de comunicar al padre que se ha terminado el tiempo y ya dependiendo del ámbito del padre, éste tendrá que hacer sus propias funcionalidades.
El desarrollo de este componente es bastante sencillo, no obstante es el más laborioso de los que hemos implementado hasta este momento en el Manual de Lit. Como siempre lo vamos a ver por partes.
Propiedades públicas y estado del componente
Hay algo que no hemos indicado todavía en el manual y que tenemos que mencionar porque este componente lo necesita. Es acerca de las propiedades públicas y el estado del componente.
- Las propiedades públicas hacen referencia a aquellas que forman parte del API del componente y que por tanto otras personas que usen el componente las pueden manipular desde fuera.
- El estado del componente hace referencia a propiedades que necesita internamente para trabajar. Pueden ser propiedades del componente igualmente, si es que necesitamos que el estado sea reactivo, pero en este caso serán propiedades privadas, que usa el componente para hacer su trabajo de manera interna y no se deberían manipular desde fuera.
Pues bien, según nos advierten en la documentación de Lit, por buenas prácticas las propiedades públicas no se deberían nunca manipular dentro del componente, cambiando sus valores. El único caso en el que se podrían cambiar las propiedades públicas es cuando se deba a la interacción del usuario. Ponen el ejemplo de un menú en el que se pueda seleccionar una opción por parte del usuario. Si es el mismo usuario el que cambió la opción del menú entonces se puede modificar. También puede que lo cambien de manera externa y nos informen del nuevo estado mediante binding.
En el caso de que tengamos necesidad de cambiar un dato del componente en su trabajo interno, deberíamos usar propiedades privadas o estado del componente. Estas propiedades generalmente las nombramos con un guión bajo como prefijo, por ejemplo: _miEstado
.
Lo cierto es que si desarrollamos en Javascript en principio no existen propiedades privadas, aunque ya el estándar las ha incorporado y podríamos disfrutar de ellas en varios navegadores. Tiene más sentido hablar de miembros privados cuando estamos en Typescript.
En nuestro componente vamos a tener estado, dado que necesitamos una propiedad interna para llevar la cuenta atrás. De hecho tendremos una pública y otra privada:
- seconds: será una propiedad pública, que forma parte del API del componente, donde esperamos que nos indiquen de manera externa el número de segundos que se deben contar.
- _countdown: será el estado del componente, una propiedad privada que usaremos internamente para llevar la cuenta atrás y que haremos decrecer a cada segundo que pase.
Configuración state de la propiedad
Como ya informamos en el primer artículo dedicado a las propiedades de los componentes, existe una configuración de las propiedades llamada "state" que nos sirve justamente para marcar aquellas que se crean para formar parte del estado del componente.
Recordemos que la configuración state a true
hace que el componente no cree un atributo en la etiqueta host para esa propiedad y por tanto no sincronice su valor si se setea desde fuera.
Así pues, veamos cómo definimos las dos propiedades de este componente, una pública y otra como estado.
static properties = {
seconds: { type: Number },
_countdown: {
type: Number,
state: true
},
}
En el constructor del componente vamos a inicializar la propiedad pública, por si no la han inicializado al usar el componente.
constructor() {
super();
this.seconds = 10;
}
La inicialización de la propiedad privada la haremos más adelante, cuando el componente esté listo para empezar la cuenta atrás.
Método firstUpdated
Otra cosa que vamos a necesitar en este componente es definir un método firstUpdated()
, que es uno de los métodos que forman parte del ciclo de vida de los componentes.
Tenemos que hablar sobre el ciclo de vida más adelante con detalle, pero podemos adelantar que firstUpdated()
se ejecutará una vez, cuando el componente haya terminado de inicializarse y se haya actualizado su template por primera vez.
En nuestro método firstUpdated
estamos seguros que se han podido inicializar todas las propiedades del API del componente y que se han sincronizado los valores de los atributos indicados en la etiqueta host. Es el momento adecuado para comenzar la cuenta atrás.
Al iniciar la cuenta atrás realizaremos estas acciones:
- Tomaremos el valor de la propiedad
seconds
y lo llevaremos al estado del componente en_countdown
. - Nos aseguraremos que
_countdown
es un número entero - Si la cuenta atrás aún no llegó a cero, entonces comenzaremos a contar hacia atrás.
- Si la cuenta atrás ya terminó (puede que seconds fuera seteado a cero o un número negativo) entonces informaremos que ya se ha terminado el tiempo.
firstUpdated() {
this._countdown = parseInt(this.seconds);
if(isNaN(this._countdown)) {
this._countdown = 10;
}
if(this._countdown > 0) {
this.decreaseCountdown();
} else {
this.informCountdownFinished();
}
}
Cómo realizamos la cuenta atrás
El método de cuenta atrás es bien sencillo. Solo decrece el estado del componente en 1 y luego comprueba si ya llegó al final.
- Si llegó al final, informará que el tiempo acabó
- Si no había llegado al final, entonces llamará al mismo método
decreaseCountdown()
pasado 1 segundo, para que continúe decreciendo esa cuenta.
decreaseCountdown() {
this._countdown--;
if(this._countdown <= 0) {
this.informCountdownFinished();
} else {
setTimeout(() => this.decreaseCountdown(), 1000);
}
}
Cómo se dispara un evento personalizado
Ya solo nos queda la parte del artículo que más nos interesaba, que consiste en avisar al componente padre, abuelo o cualquiera de sus ancestros, que la cuenta atrás ha finalizado.
Las comunicaciones de hijos hacia padres o ancestros se hace disparando eventos. En este caso se tratará de un evento personalizado, propio de este componente.
Los eventos personalizados se nombran preferentemente con el nombre del propio componente, seguido de la descripción de aquello que ha ocurrido. En este caso usaremos "dw-countdown-finished
" como nombre del evento.
Ese nombrado no es obligatorio, de hecho podríamos usar cualquier nombre. Sin embargo, para asegurarnos que no colisionen los eventos personalizados de nuestros componentes con los eventos personalizados de componentes de terceros, es una buena idea siempre prefijarlos. Nosotros como costumbre usamos el prefijo del nombre del componente, pero es solo una alternativa que creemos bastante adecuada para asegurarnos que no haya problemas.
informCountdownFinished() {
this.dispatchEvent(new CustomEvent('dw-countdown-finished', {
bubbles: true,
composed: true,
detail: {
seconds: this.seconds
}
}));
}
- Usamos
bubbles
para que el evento se propague por todo el árbol DOM hacia arriba. - Usamos
composed
para que el evento traspase los posibles shadow-dom que tenga cuando escale hacia arriba. - Usamos
detail
para informar de cualquier cosa que necesitemos que sepan los padres. En este caso estamos informando de los segundos que pasaron, es decir, de cómo se creó inicialmente la propiedad pública seconds.
Código completo del componente de cuenta atrás
Con esto ya hemos terminado las cosas que deberías saber sobre este componente, así que vamos a mostrarte el código completo para que lo veas de una manera más global.
import { LitElement, html, css } from 'lit';
export class DwCountdown extends LitElement {
static styles = [
css`
:host {
display: inline-block;
padding: 8px;
background-color: red;
color: white;
border-radius: 4px;
}
`
];
static properties = {
seconds: { type: Number },
_countdown: {
type: Number,
state: true
},
}
constructor() {
super();
this.seconds = 10;
}
firstUpdated() {
this._countdown = parseInt(this.seconds);
if(isNaN(this._countdown)) {
this._countdown = 10;
}
if(this._countdown > 0) {
this.decreaseCountdown();
} else {
this.informCountdownFinished();
}
}
render() {
return html`
${this._countdown}
`;
}
decreaseCountdown() {
this._countdown--;
if(this._countdown <= 0) {
this.informCountdownFinished();
} else {
setTimeout(() => this.decreaseCountdown(), 1000);
}
}
informCountdownFinished() {
this.dispatchEvent(new CustomEvent('dw-countdown-finished', {
bubbles: true,
composed: true,
detail: {
seconds: this.seconds
}
}));
}
}
customElements.define('dw-countdown', DwCountdown);
Cómo se captura un evento personalizado
La captura de eventos personalizados se realiza igual que la captura de cualquier otro evento de Javascript, ya que éstos son simplemente eventos Javascript nativos. Sin embargo, si estamos dentro de componentes Lit podemos capturar eventos de dos maneras distintas:
- A través de la declaración de un manejador en el template de un componente que lo necesite capturar, con la sintaxis "
@
". - Capturamos eventos mediante el propio Javascript nativo, de manera imperativa, con
addEventListener()
.
En este caso vamos a capturar el evento desde otro componente, por lo que podemos definir el manejador de manera declarativa.
<dw-countdown seconds="5" @dw-countdown-finished=${this.onFinished}></dw-countdown>
Como puedes ver, usamos @ y luego el nombre del evento personalizado que queremos escuchar. Asignamos el método del componente que tiene el código del manejador.
En ese evento podríamos perfectamente obtener el "detail", con cualquier dato que tuviera, mediante el objeto evento nativo de Javascript.
onFinished(event) {
console.log(`La cuenta atrás acabó y se contaron ${event.detail.seconds} segundos.`);
}
Conclusión
El objetivo de este artículo era explicar cómo se comunican los componentes de hijos a padres, completando las posibles direcciones que pueden producirse durante su interoperabilidad. Sin embargo, el ejemplo que hemos escogido nos ha dado pie a para aprender varias cosas útiles como disparar eventos personalizados, capturar esos eventos recibiendo datos que puedan enviar, cómo definir estado en los componentes y cómo realizar operaciones cuando los componentes se han inicializado.
Ha sido por tanto un artículo muy provechoso. Esperamos que os haya gustado. El código completo de lo que hemos realizado hasta aquí lo tienes en GitHub.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...