Como comentamos en un capítulo anterior, mediante las propiedades reactivas tenemos una forma práctica y cómoda de trabajar con propiedades en un componente LitElement de Lit. Pero vamos a profundizar un poco en la forma de trabajar con dichas propiedades reactivas.

Propiedades y atributos en Lit / LitElement

Diferencias

En primer lugar, es importante recalcar (sobre todo a desarrolladores que provienen de React u otros frameworks) que una propiedad no es lo mismo que un atributo HTML. Una propiedad es una variable que existe en una clase (y que puede tener cualquier tipo de dato), mientras que un atributo HTML siempre contendrá valores de tipo .

Es muy probable que esta confusión se deba al uso de tecnologías no estándar como JSX, que aparentan trabajar con HTML, pero en realidad se trata de Javascript con azúcar sintáctico para hacerlo muy parecido a HTML y preprocesarlo con otras herramientas externas.

Veamos un ejemplo para ver bien las diferencias entre propiedades y atributos:

<app-element value="42"></app-element>

En este caso, el componente app-element tiene un atributo value que contiene 42. Aunque este valor es en sí un número, recuerda que en Javascript lo recibirás como un . Sin embargo, dicho componente puede tener también una propiedad con el mismo nombre, a la que se puede acceder mediante this.value en el interior del componente, y que en principio, no tendría relación con el atributo, posibilitando incluso tener valores diferentes.

Veámoslo en funcionamiento en este componente nativo (sin LitElement):

<app-element value="42"></app-element>

<script>
  customElements.define("app-element", class extends HTMLElement {
    connectedCallback() {
      this.value = 55;
      this.innerHTML = `
        Propiedad: ${this.value}
        Atributo: ${this.getAttribute("value")}
      `;
    }
  });
</script>

La propiedad value contiene el 55, mientras que el atributo value contiene el 42.

Lo mismo ocurre en un componente LitElement en el que no hemos definido propiedades reactivas. Podemos utilizar propiedades en el componente, pero seguirán funcionando igual que en un componente nativo, es decir, de forma independiente al atributo con el mismo nombre:

<app-element value="42"></app-element>

<script type="module">
  import { LitElement, html } from 'https://unpkg.com/lit?module';

  customElements.define("app-element", class extends LitElement {
    render() {
      this.value = 55;
      return html`
        Propiedad: ${this.value}
        Atributo: ${this.getAttribute("value")}
      `;
    }
  });
</script>

Este comportamiento puede llegar a cambiar en Lit cuando indicas que una propiedad es reactiva. Así pues, podríamos tener propiedades y atributos con el mismo nombre y valores independientes, con los valores sincronizados (mismo valor), o que se sincronizan cuando cambias el valor de la propiedad (o del atributo).

Propiedades reactivas

En componentes basados en LitElement basta con definir un getter estático properties() en la clase de nuestro componente. Este getter deberá devolver un donde incluiremos todas las propiedades que queramos convertir en propiedades reactivas.

Por ejemplo, vamos a adaptar el ejemplo anterior convirtiendo la propiedad value en reactiva e introduciendo varios cambios:

  • El getter properties donde convertimos value en una propiedad reactiva de tipo .
  • El método updateValue() que actualiza el atributo value con un valor entre 1 y 6.
  • Un evento @click que llama a updateValue() cuando pulsas el botón.
<app-element value="42"></app-element>

<script type="module">
  import { LitElement, html } from 'https://unpkg.com/lit?module';

  customElements.define("app-element", class extends LitElement {
    static get properties() {
      return {
        value: { type: Number }
      }
    }

    updateValue() {
      this.setAttribute("value", 1 + ~~(Math.random() * 5));
    }

    render() {
      return html`<div>
        <button @click=${this.updateValue}>Actualizar atributo</button>
        Propiedad: ${this.value}
        Atributo: ${this.getAttribute("value")}
      </div>`;
    }
  });
</script>

Por defecto, al convertir la propiedad en una propiedad reactiva, el atributo con el mismo nombre se asocia a dicha propiedad, y si cambia el atributo, se actualiza también la propiedad con el valor del atributo. Es lo que se llama observación de atributos, y viene activa por defecto.

Por otro lado, de forma opuesta, también es posible que cuando ocurran cambios en la propiedad, se actualice también el atributo con el nuevo valor de la propiedad. Es lo que se llama reflejar propiedades, y viene desactivado por defecto.

Si nos fijamos en el getter properties (que es donde se configuran todas estas opciones) la forma más corta de definir propiedades reactivas suele ser incluir sólamente su tipo con type, como hemos hecho hasta ahora.

Sin embargo, existen más características a añadir a nuestras propiedades reactivas:

CaracterísticaDescripción
type tipoIndica el tipo de dato de la propiedad: String, Number, Boolean, Object, etc...
attribute Observa el atributo del mismo nombre y aplica cambios a la propiedad. Por defecto, true.
attribute Observa el atributo con el nombre específico y aplica cambios a la propiedad.
reflect Refleja los cambios de una propiedad en su atributo observado. Por defecto, false.
converter Transforma el valor del atributo y lo guarda en la propiedad.
converter Transforma el valor del atributo y lo guarda en la propiedad y/o viceversa.
hasChanged Permite comprobar si la propiedad ha cambiado en la última actualización.
noAccessor Evita generar los getters/setters que LitElement usa internamente. Por defecto, false.

Las características attribute y reflect son las que comentamos más atrás. Observa que attribute puede definirse tanto con (activando o desactivando la característica) como con , donde asociaríamos una propiedad con un atributo con diferente nombre.

También tenemos a nuestra disponibilidad hasChanged(old, current), donde podemos definir una función que se ejecutará en cada actualización y donde se puede comprobar si el valor de la propiedad ha cambiado en esa última actualización.

Por último, noAccessor nos permite desactivar los getters/setters internos de LitElement, en el caso que no queramos utilizarlos, lo cuál no suele ser muy común.

Conversores por defecto

Como hemos comentado más atrás, un atributo HTML siempre contiene un . Sin embargo, debido a las conversiones por defecto que aplica Lit internamente, es posible indicar valores más complejos en los atributos HTML, de forma que terminen aplicándose en una propiedad con el tipo de dato correcto correspondiente:

<app-element num="45" str="45" obj='{"stuff":"hi"}' arr="[1,2,3]"></app-element>

<script type="module">
  import { LitElement, html } from 'https://unpkg.com/lit?module';

  customElements.define("app-element", class extends LitElement {
    static get properties() {
      return {
        num: { type: Number },
        str: { type: String },
        obj: { type: Object },
        arr: { type: Array }
      }
    }

    render() {
      return html`<div>
        <ul>
          <li>${this.num} es de tipo ${this.num.constructor.name}.</li>
          <li>${this.str} es de tipo ${this.str.constructor.name}.</li>
          <li>${this.obj} es de tipo ${this.obj.constructor.name}.</li>
          <li>${this.arr} es de tipo ${this.arr.constructor.name}.</li>
        </ul>
      </div>`;
    }
  });
</script>

Observa que en este ejemplo hemos convertido en propiedades reactivas cada una de las propiedades num, str, obj y arr. Salvo que se especifique lo contrario, cada una de ellas tiene por defecto el attribute a true, por lo que los atributos son observados y actualizados en su respectiva propiedad, aplicando un conversor por defecto.

De esta forma, si accedemos a ver el tipo de dato de cada propiedad, veremos que se han aplicado correctamente. Sin embargo, si intentas hacer lo mismo con los atributos HTML, recuerda que siempre obtendremos que se trata de un .

Conversor personalizado

Podemos crearnos nuestros propios conversores personalizados de propiedades con la característica converter, donde podemos utilizar una de las dos modalidades siguientes:

  • Aplicar un con las claves fromAttribute y toAttribute. Cada una de ellas, contendrá una función conversora personalizada que implementaremos como queramos.

  • Aplicar una , que es la versión corta de la anterior, donde estariamos definiendo una sola función conversora personalizada, concretamente para fromAttribute.

Veamos un ejemplo utilizando un converter de tipo :

<app-element initial-value="42"></app-element>

<script type="module">
  import { LitElement, html } from 'https://unpkg.com/lit-element?module';

  customElements.define("app-element", class extends LitElement {
    static get properties() {
      return {
        value: {
          type: String,
          attribute: "initial-value",
          reflect: true,
          converter: {
            fromAttribute: (number) => `${number}cm`,
            toAttribute: (number) => `${number}px`
          }
        }
      }
    }

    updateValue() {
      this.setAttribute("initial-value", 1 + ~~(Math.random() * 5));
    }

    render() {
      return html`<div>
        <button @click=${this.updateValue}>Actualizar atributo</button>
        Propiedad: ${this.value}
        Atributo: ${this.getAttribute("initial-value")}
      </div>`;
    }
  });
</script>

Recalcar que con attribute hemos modificado la asociación propiedad-atributo value por una asociación de la propiedad value y el atributo initial-value. Por otro lado, con reflect a true, hemos indicado que las propiedades al cambiar su valor, deben reflejarse en los atributos.

Tras estas primeras características, usamos converter con un con 2 claves:

  • La primera de ellas, fromAttribute, se aplica cuando se detecta un cambio en el atributo, añadiendo la unidad cm al valor y guardándolo en la propiedad.

  • La segunda de ellas, toAttribute, se aplica cuando se detecta un cambio en la propiedad, añadiendo la unidad px al valor y guardándolo en el atributo.

Recuerda que en el caso de que no se indique un sino una función en converter, sería el equivalente a sólo tener fromAttribute en la modalidad del objeto.

¿Quién soy yo?

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