El método updated() del ciclo de vida nos permite por tanto estar atentos a cualquier cambio en los valores de las propiedades, para realizar las acciones pertinentes cuando éstos ocurran.
En el Manual de Lit hemos explicado diversos aspectos fundamentales sobre el ciclo de vida de los componentes. Hemos conocido métodos tan importantes como los que nos ofrece el propio estándar Web Components, o el método firstUpdated() introducido por Lit. Sin embargo también existen métodos específicos para el control del ciclo de vida de las propiedades de los elementos personalizados.
Las propiedades de los componentes Lit tienen también su propio ciclo de vida, gracias al cual podemos estar atentos a diversas situaciones que ocurran con ellas, generalmente cambios en sus valores. Vamos a ir conociendo alguno de los métodos de los que disponemos, con nuevos ejemplos de componentes que los utilizan.
Para explicar el Ciclo de vida de las propiedades de los componentes Lit vamos a tratar estos puntos de interés.
Método updated() del ciclo de vida en Lit
La librería Lit introduce el método updated()
para el control de las actualizaciones de propiedades dentro de un componente. Este método se invoca cada vez que cualquiera de las propiedades de un componente cambia, en el preciso instante posterior a la actualización del template.
Para que nos aclaremos,
updated()
se invoca cuando cambian las propiedades reactivas del componente, justo después de que su template se haya actualizado. Porque recordemos que en la clase del componente podemos tener propiedades, como cualquier otra clase de programación orientada a objetos, pero si no las declaramos en el objeto de properties en realidad no serán reactivas.
Por qué necesitamos el método updated
Es verdad que las propiedades reactivas actualizan el template del componente sin que tengas que hacer nada en particular. Pero puede que exista una necesidad especial en un componente por la cual necesites hacer cosas cuando una o varias de sus propiedades cambien.
Por supuesto, el método render()
del componente que se usa para definir su vista no es un buen lugar para realizar algún tipo de efecto colateral cuando cambian las propiedades, ya que debería ocuparse de una renderización y nunca de hacer cambios en el componente en sí. Así que usaremos el método updated()
para centralizar las acciones que puedan ser necesarias de realizar por la lógica del componente cuando una propiedad ha cambiado, así podremos permanecer atentos a los cambios que nos interesan.
updated(changedProperties) {
// Ha ocurrido algún cambio en una propiedad y se ha actualizado el template
}
Mapa de propiedades cambiadas
El método updated()
se invoca automáticamente cuando cambia cualquiera de las propiedades, independientemente de cuál ha sido la propiedad que ha cambiado. Sin embargo, generalmente queremos hacer cosas cuando ha cambiado exactamente una de las propiedades del componente, y no todas.
Para reconocer la propiedad que ha cambiado usamos el mapa de propiedades que nos pasan por parámetro en el método updated
. El parámetro que nos mandan es un objeto de tipo map de Javascript ("map" es un tipo de objeto estándar que nos sirve para hacer mapas de claves y sus valores).
La manera de saber si ha cambiado una propiedad en particular es usar el método has()
del mapa de propiedades.
updated(changedProperties) {
if(changedProperties.has('seconds')) {
// Ha cambiado la propiedad "seconds"
}
}
En este método estamos reconociendo cuándo ha cambiado la propiedad "seconds" del componente.
El parámetro lo podemos llamar como queramos, ya que es un simple parámetro, pero lo normal es que lo nombremos como changedProperties.
Ejemplo de componente con updated
Ahora vamos a ver un ejemplo de componente que usa el método updated()
del ciclo de vida, para poder practicar con él.
Vamos a rescatar un componente que ya habíamos realizado, pero lo vamos a mejorar agregando algunas cosas extra. Se trata del componente dw-countdown que vimos cuando explicamos la comunicación de componentes hijos a padres por medio de eventos personalizados.
En esta mejora al componente le vamos a agregar:
- Una propiedad "
active
" que nos permite activar o desactivar la cuenta atrás, parando el proceso de decremento de la cuenta - Estar atento a posibles cambios en la propiedad "
seconds
", por si manipulan desde fuera esa propiedad.
Un caso de uso típico donde resulta especialmente útill del método updated()
del ciclo de vida se produce cuando los cambios a las propiedades pueden venir de muchos orígenes. Gracias a updated()
puedo suscribirme al cambio en la propiedad sin importarme del motivo de ese cambio, lo que me permite centralizar el código que tiene que producirse como respuesta al cambio de una propiedad. Es decir, podré poner todos los tratamientos derivados de los cambios en un único sitio en el componente, en vez de mantener el control desde varios lugares.
En este caso usar el método updated()
me faciltaría la labor de desarrollo porque necesito parar o reiniciar la cuenta atrás del componente por diversos motivos:
- Se ha producido un cambio en la propiedad
active
- Necesito parar la cuenta atrás si se ha llegado al final
- Necesito reiniciar la cuenta atrás cuando la propiedad "
active
" ha cambiado a true y además aún quedan segundos por restar.
De este modo, estamos viendo que updated()
podría servirnos para centralizar el inicio, la parada o el reinicio de la cuenta atrás.
Ahora que hemos aclarado algunos puntos interesantes sobre cómo debe comportarse el componente, vamos a comenzar a ver el código. Comenzamos por las propiedades que tendrá el componente.
static properties = {
seconds: { type: Number },
active: {
type: Boolean,
reflect: true
},
}
En esta ocasión tenemos dos propiedades:
- Una es
seconds
, que mantendrá el número de segundos de la cuenta atrás. Esta propiedad pertenece al API del componente. Por ello, se podría cambiar esa propiedad desde cualquier lugar donde se use el componente. Por supuesto, sea quien sea que cambie esa propiedad, el componente de cuenta atrás debe reaccionar convenientemente y no romperse! - La otra propiedad es
active
, que también formará parte del API del componente, por lo que cualquier agente que use este componente podrá activar o no la cuenta atrás.
En el constructor vamos a inicializar estas propiedades.
constructor() {
super();
this.seconds = 0;
this.active = false;
this._interval = null;
}
Como puedes ver, si no se setean desde fuera el componente comenzará con segundos a cero y la cuenta atrás estará inactiva. Además, hemos creado una propiedad privada y no reactiva que se llama _interval
. Esta propiedad me permitirá almacenar un objeto devuelto por setInterval()
para poder parar la cuenta atrás en cualquier momento.
Usando setInterval
En esta ocasión vamos a usar el método setInterval()
para mantener el proceso periódico de restarle 1 segundo a la cuenta atrás. Es un cambio con respecto al componente de cuenta atrás que habíamos desarrollado antes, donde usábamos setTimeout
.
Para gestionar el intervalo periódico de la cuenta atrás vamos a tener dos métodos privados:
_createInterval() {
if(!this._interval) {
this._interval = setInterval(() => this.seconds--, 1000);
}
}
_cancelInterval() {
if(this._interval) {
clearInterval(this._interval);
}
this._interval = null;
}
Como te puedes imaginar, _createInterval
comenzará la cuenta atrás y _cancelInterval
detendrá la cuenta atrás.
El método _createInterval
solamente se encarga de setear un intervalo de tiempo con setInterval
, en el que se restará un segundo cada 1000 milisegundos. Pero la clave de este método es que almacena una referencia al intervalo, para poder pararlo cuando sea necesario. Para almacenar esa referencia es necesario la propiedad privada no reactiva _interval
.
_cancelInterval
primero comprueba si existe un intervalo programado, en cuyo caso lo para con clearInterval
y además pone a null
el intervalo para volver a setearlo más adelante.
Cómo aplicamos el método updated()
Ahora solo nos queda por ver el método updated()
que nos permite centralizar todo el flujo de cambio de propiedades, tal como hemos descrito antes.
updated(changedProperties) {
if(changedProperties.has('seconds')) {
if(this.seconds <= 0) {
this.finish();
}
}
if(changedProperties.has('active')) {
if(this.active) {
if(this.seconds > 0) {
this._createInterval();
}
if(this.seconds === 0) {
this.active = false;
}
} else {
this._cancelInterval();
}
}
}
Este método hace dos bloques de trabajo:
1.- Comprueba si hay cambios en seconds()
En ese caso tenemos que verificar si se ha llegado al final de la cuenta atrás para pararla. Conseguimos parar la cuenta con el método finish()
que veremos a continuación:
finish() {
this.dispatchEvent(new CustomEvent('dw-countdown-finished', {
bubbles: true,
composed: true,
}));
this._cancelInterval();
this.active = false;
this.seconds = 0;
}
Simplemente se encarga de avisar a los componentes que lo usan que se ha llegado al final. Además se cancela el intervalo y se ponen las propiedades active
a false y seconds
a cero.
2.- Comprueba si hay cambios en la propiedad active
En este caso verifica si la propiedad se evalúa como true (lo que quiere decir que habría pasado de false
a true
). En este caso es que se está intentando reiniciar una cuenta atrás, por ello se comprueba que:
- Si los segundos son mayores que cero, es que se tiene que reiniciar la cuenta atrás.
- Si los segundos es igual a cero, es que no quedaba tiempo por contar hacia atrás, por lo que volvemos a poner active a false.
Si la propiedad active
esta a false
(lo que quiere decir que habría cambiado de true a false), simplemente tenemos que parar la cuenta atrás.
Eliminar el interval cuando el elemento sale del DOM
Solamente nos quedaría tomar en cuenta en consideración un detalle que también tiene que ver con el ciclo de vida, para realizar acciones cuando el elemento lo retiramos del DOM.
Resulta que al haber realizado un intervalo con setInterval
se han quedado programados unos comportamientos periódicos. Ahora vamos a suponer que mientras que la cuenta atrás está activa el componente se quita de la página. En ese caso sería importante detener también la cuenta atrás porque no tiene sentido dejar un código ejecutándose a cada segundo que pasa, con un componente de cuenta atrás que ya no está en la página por ningún lado.
Para hacer este tipo de cosas tenemos el método disconnectedCallback(), que podemos usar de esta manera.
disconnectedCallback() {
this._cancelInterval();
}
En artículos anteriores puedes acceder a más información y ejemplos de disconnectedCallback.
Código completo de este componente
Ya para acabar, vamos a ver el código completo de este componente que acabamos de realizar.
import { LitElement, html, css } from 'lit';
export class DwSecondsCountdown extends LitElement {
static styles = [
css`
:host {
display: block;
font-size: var(--dw-seconds-countdown-font-size, 3rem);
}
span {
color: var(--dw-seconds-countdown-inactive-font-color, #999);
}
:host([active]) span {
color: var(--dw-seconds-countdown-font-color, #de2626);
}
`
];
static properties = {
seconds: { type: Number },
active: {
type: Boolean,
reflect: true
},
}
constructor() {
super();
this.seconds = 0;
this.active = false;
this._interval = null;
}
render() {
return html`
<span>
${this.seconds}
</span>
`;
}
_createInterval() {
if(!this._interval) {
this._interval = setInterval(() => {
this.seconds--;
console.log('resto seconds', this.seconds);
}
, 1000);
}
}
_cancelInterval() {
if(this._interval) {
clearInterval(this._interval);
}
this._interval = null;
}
updated(changedProperties) {
if(changedProperties.has('seconds')) {
if(this.seconds <= 0) {
this.finish();
}
}
if(changedProperties.has('active')) {
if(this.active) {
if(this.seconds > 0) {
this._createInterval();
}
if(this.seconds === 0) {
this.active = false;
}
} else {
this._cancelInterval();
}
}
}
finish() {
this.dispatchEvent(new CustomEvent('dw-countdown-finished', {
bubbles: true,
composed: true,
}));
this._cancelInterval();
this.active = false;
this.seconds = 0;
}
disconnectedCallback() {
this._cancelInterval();
}
}
customElements.define('dw-seconds-countdown', DwSecondsCountdown);
Acceso a los valores antiguos de las propiedades desde updated
No quería finalizar este artículo sobre el método updated
sin explicar que el mapa de propiedades cambiadas nos puede indicar el valor anterior que tenía la propiedad antes del cambio.
Este valor no lo hemos llegado a usar en el ejemplo del componente visto en este artículo, pero podría ser importante para otros componentes que necesitarás desarrollar en el futuro.
Justamente el objeto map que recibimos en el parámetro de las propiedades cambiadas tiene todos los valores antiguos de las propiedades que hayan cambiado en la última actualización.
Usaremos el método get()
del objeto map para obtener uno de sus valores. Un ejemplo de uso de get()
para el acceso a los valores del objeto map, de modo que podremos recuperar el valor anterior de la propiedad, lo podemos ver aquí:
updated(changedProperties) {
if(changedProperties.has('active')) {
console.log(`Ha cambiado la propiedad active. El valor antiguo es ${changedProperties.get('active')} y el valor nuevo es ${this.active}`);
}
}
A la vista del ejemplo anterior, si hemos determinado que changedProperties
tiene la propiedad "active" es que ha cambiado esa propiedad. Entonces el valor anterior lo obtengo con changedProperties.get('active')
, mientras que el valor nuevo de la propiedad lo encuentras como dato mediante this.active
.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...