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.
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 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
Por ejemplo, vamos a adaptar el ejemplo anterior convirtiendo la propiedad value
en reactiva e introduciendo varios cambios:
- El getter
properties
donde convertimosvalue
en una propiedad reactiva de tipo. - El método
updateValue()
que actualiza el atributovalue
con un valor entre1
y6
. - Un evento
@click
que llama aupdateValue()
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ística | Descripción |
---|---|
type tipo | Indica 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
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
<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
ytoAttribute
. 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
La primera de ellas,
fromAttribute
, se aplica cuando se detecta un cambio en el atributo, añadiendo la unidadcm
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 unidadpx
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 tenerfromAttribute
en la modalidad del objeto.