> Manuales > Manual de Lit

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.

Ciclo de vida de las propiedades de los componentes Lit

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:

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:

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:

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 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...

Manual