En l'última lliçó del mòdul anterior es va identificar el problema amb precisió: <task-card> i <task-list> utilitzen camps d'instància normals (this.titulo, this.estado, this.tareas...), i aquests camps no disparen cap actualització perquè Lit no té cap mecanisme instal·lat per vigilar-los. Aquesta lliçó resol exactament aquesta mancança: aprendràs a declarar propietats reactives de veritat amb static properties, entendràs què passa per dins quan es declara una propietat d'aquesta manera, i convertiràs <task-card> perquè titulo, estado, prioridad i urgente deixin de ser camps solts i passin a ser propietats reactives reconegudes per Lit.
Continguts
- Què significa exactament que una propietat sigui "reactiva"
- Declarar propietats amb
static properties - Les opcions de configuració:
type,attribute,reflect - L'alternativa amb el decorador
@property - Convertint
<task-card>a propietats reactives reals - Utilitzar
<task-card>des d'HTML (atributs) i des de JavaScript (propietats) - Què passa per dins: el mecanisme dels accessors
- Què significa exactament que una propietat sigui "reactiva"
En l'apartat 1 de l'última lliçó del mòdul 2 es va explicar que render() no es crida sol: necessita un disparador, i que aquest disparador habitual és un canvi en una propietat reactiva. Ara toca precisar què significa exactament aquesta paraula, "reactiva", aplicada a una propietat d'un component Lit.
Una propietat reactiva és una propietat de la classe que Lit coneix explícitament perquè s'ha declarat com a tal (amb static properties, o amb el decorador @property, que es veurà a l'apartat 4). En declarar-la, Lit hi instal·la un mecanisme especial —un getter i un setter, com es detallarà a l'apartat 7— que li permet detectar automàticament quan canvia el seu valor. En el moment en què aquest canvi es detecta, Lit programa una actualització del component, seguint exactament el procés asíncron i agrupat que es va explicar a la lliçó del cicle de renderitzat.
La diferència amb un camp d'instància normal, com els que s'han utilitzat durant tot el mòdul 2, és justament aquesta: un camp d'instància (this.titulo = 'algo' sense més) és una assignació de JavaScript corrent, invisible per a Lit. Una propietat reactiva és una assignació que passa per un mecanisme de Lit capaç de reaccionar-hi. El nom de la propietat pot ser idèntic en ambdós casos (this.titulo); el que canvia és si aquesta propietat ha estat declarada d'entrada a static properties (o amb @property) o no.
- Declarar propietats amb
static properties
static propertiesLa manera més directa de declarar propietats reactives, sense necessitat de cap pas de compilació addicional (com ja es va avançar a la lliçó d'anatomia del mòdul 1), és un camp estàtic de la classe anomenat properties, que conté un objecte: una clau per cada propietat que es vol declarar, i un valor que descriu com s'ha de comportar aquesta propietat.
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
};
constructor() {
super();
this.titulo = 'Tarea sin título';
this.estado = 'pendiente';
}
render() {
return html`<h3>${this.titulo}</h3><p>${this.estado}</p>`;
}
}
customElements.define('task-card', TaskCard);Dos detalls importants en aquest exemple:
static propertieses declara una vegada, fora de qualsevol mètode, com un camp estàtic de la classe (amb la sintaxi moderna de camps de classe de JavaScript, disponible sense transpilació en qualsevol navegador modern). No és un mètode, ni s'executa cada vegada que canvia alguna cosa: és simplement la llista de "quines propietats existeixen i com són", que Lit llegeix una única vegada, en definir la classe.- El valor inicial de cada propietat es continua assignant al
constructor, exactament igual que amb els camps d'instància del mòdul anterior.static propertiesno assigna cap valor per si sola: només declara l'existència de la propietat i la seva configuració. És responsabilitat delconstructor(després de la crida obligatòria asuper()) donar-li un valor inicial raonable.
A partir d'aquest moment, this.titulo i this.estado ja no són camps d'instància corrents: són propietats reactives. Qualsevol assignació posterior (elemento.titulo = 'Otro título', tant si és des de dins de la classe com des de fora) passarà pel mecanisme de Lit i disparà una actualització.
- Les opcions de configuració:
type, attribute, reflect
type, attribute, reflectL'objecte de configuració de cada propietat ({ type: String } en els exemples anteriors) admet diverses claus. Les tres més rellevants en aquest punt del curs són type, attribute i reflect; les dues primeres es veuen aquí amb detall, i reflect es reprèn en profunditat a la lliçó "Atributs vs Propietats i Reflexió" d'aquest mateix mòdul, així que aquí n'hi ha prou amb una introducció.
| Opció | Per a què serveix | Valor per defecte |
|---|---|---|
type |
Indica a Lit quin tipus de dada té la propietat (String, Number, Boolean, Array, Object), per poder convertir correctament entre l'atribut HTML (sempre text) i el valor JavaScript de la propietat |
String si no s'indica |
attribute |
Controla si la propietat té un atribut HTML associat, i amb quin nom. Per defecte, Lit crea automàticament un atribut amb el mateix nom que la propietat, en minúscules | true (mateix nom, en minúscules) |
reflect |
Controla si, a més de llegir l'atribut cap a la propietat, Lit també escriu de tornada l'atribut quan canvia la propietat des de JavaScript | false |
Un exemple amb les tres opcions alhora, sobre una propietat fictícia prioridadAlta:
static properties = {
prioridadAlta: {
type: Boolean,
attribute: 'prioridad-alta', // l'atribut HTML es dirà "prioridad-alta", no "prioridadalta"
reflect: true, // canviar la propietat des de JS també actualitza l'atribut
},
};Fixa't en el nom de l'atribut: com que HTML no distingeix majúscules de minúscules en els noms d'atribut, una propietat amb nom en camelCase com prioridadAlta necessitaria, si es deixés el valor per defecte, convertir-se en prioridadalta (tot junt, en minúscules), la qual cosa és difícil de llegir. Per això, en propietats amb noms compostos, és habitual indicar explícitament el nom d'atribut desitjat en format kebab-case (prioridad-alta), com s'ha fet aquí.
Si no interessa que una propietat tingui cap atribut associat —per exemple, perquè només té sentit assignar-la des de JavaScript, mai des d'HTML—, es pot desactivar completament amb attribute: false. En aquest cas, la propietat continua sent reactiva (Lit continua vigilant els seus canvis des de JavaScript), però no hi ha manera d'establir el seu valor inicial mitjançant un atribut HTML.
- L'alternativa amb el decorador
@property
@propertyCom ja es va apuntar a la lliçó d'anatomia del mòdul 1, Lit ofereix una sintaxi alternativa, equivalent en comportament, basada en decoradors de TypeScript o de JavaScript amb Babel:
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('task-card')
class TaskCard extends LitElement {
@property({ type: String })
titulo = 'Tarea sin título';
@property({ type: String })
estado = 'pendiente';
render() {
return html`<h3>${this.titulo}</h3><p>${this.estado}</p>`;
}
}El decorador @property rep exactament el mateix objecte de configuració que una entrada de static properties (type, attribute, reflect), i el valor inicial s'assigna directament al costat de la declaració del camp, sense necessitat d'un constructor explícit. El resultat en temps d'execució és idèntic al de static properties: ambdues sintaxis acaben generant el mateix mecanisme intern de propietats reactives.
Aquest curs continua utilitzant static properties com a estil principal, perquè no requereix cap pas de compilació addicional (tal com es va explicar a la lliçó de configuració de l'entorn del mòdul 1), però és important reconèixer la sintaxi amb decoradors perquè apareix molt sovint a la documentació oficial de Lit i en projectes reals que utilitzen TypeScript.
- Convertint
<task-card> a propietats reactives reals
<task-card> a propietats reactives realsAmb la teoria ja coberta, és el moment de fer el pas que es va anunciar en tancar el mòdul 2: convertir els camps d'instància de <task-card> (titulo, estado, prioridad, urgente) en propietats reactives declarades amb static properties. Recupera el fitxer src/components/task-card.js tal com va quedar al final del mòdul 2, amb la insígnia d'estat i l'avís d'urgència, i afegeix la declaració de propietats:
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
urgente: { type: Boolean },
};
constructor() {
super();
this.titulo = 'Tarea sin título';
this.estado = 'pendiente'; // 'pendiente' | 'en-progreso' | 'hecha'
this.prioridad = 3;
this.urgente = false;
}
renderInsigniaEstado() {
if (this.estado === 'hecha') {
return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
}
if (this.estado === 'en-progreso') {
return html`<span class="insignia insignia--progreso">◐ En progreso</span>`;
}
return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
}
render() {
return html`
<article>
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
<p>Prioridad: ${this.prioridad}</p>
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
</article>
`;
}
}
customElements.define('task-card', TaskCard);Compara aquest codi amb el de la lliçó "Renderitzat Condicional" del mòdul 2: la plantilla (render(), renderInsigniaEstado()) no ha canviat ni una línia. L'únic de nou és el bloc static properties, que declara explícitament les quatre propietats, i la incorporació de prioridad a la plantilla (abans no es mostrava enlloc, ja que no tenia sentit ensenyar un valor que no podia canviar de manera reactiva). Aquesta és una idea que convé retenir: afegir reactivitat no exigeix reescriure la plantilla; render() continua llegint this.titulo, this.estado, etc., exactament igual que abans. El que canvia és que, ara, escriure en aquestes propietats sí que provoca que render() es torni a executar.
- Utilitzar
<task-card> des d'HTML (atributs) i des de JavaScript (propietats)
<task-card> des d'HTML (atributs) i des de JavaScript (propietats)Amb les propietats ja declarades, <task-card> pot rebre els seus valors de dues maneres diferents, i és important distingir-les amb claredat.
Des d'HTML, mitjançant atributs (sempre com a text, per això només té sentit per a titulo i estado, el tipus declarat dels quals és String; per a prioridad, de tipus Number, i urgente, de tipus Boolean, Lit s'encarrega de convertir el text de l'atribut al tipus declarat, com es detallarà a la lliçó següent):
Des de JavaScript, mitjançant la propietat del DOM (amb qualsevol tipus de dada, sense passar per text):
const tarjeta = document.querySelector('task-card');
tarjeta.titulo = 'Revisar el PR de autenticación';
tarjeta.estado = 'pendiente';
tarjeta.prioridad = 5;
tarjeta.urgente = true;I, com ja es va veure a la lliçó d'expressions i interpolació del mòdul 2, també es pot assignar una propietat directament dins d'una plantilla html utilitzant el prefix de punt:
Amb les propietats declarades com a reactives, qualsevol d'aquestes tres maneres d'assignar un valor —atribut HTML en crear l'element, propietat de JavaScript en qualsevol moment, o interpolació amb punt dins d'una plantilla html— dispara ara una actualització real: la pròxima vegada que Lit executi render(), la targeta mostrarà els nous valors. Aquesta és, exactament, la peça que faltava durant tot el mòdul 2.
- Què passa per dins: el mecanisme dels accessors
Per acabar de desmitificar la "màgia" de les propietats reactives, convé entendre, a un nivell raonable de detall, què fa Lit realment quan processa static properties. Per cada propietat declarada, Lit instal·la sobre el prototip de la classe un parell de funcions especials de JavaScript anomenades getter i setter (mitjançant Object.defineProperty, una funcionalitat estàndard del llenguatge, no específica de Lit), en lloc de deixar que titulo sigui un camp normal.
De manera simplificada, és com si Lit generés automàticament alguna cosa equivalent a això per a la propietat titulo:
// Simplificació del que Lit fa internament per cada propietat declarada
get titulo() {
return this._titulo;
}
set titulo(valorNuevo) {
const valorAnterior = this._titulo;
this._titulo = valorNuevo;
this.requestUpdate('titulo', valorAnterior); // el disparador vist al mòdul 2
}Quan al codi s'escriu this.titulo = 'Nuevo título', en realitat no s'està assignant directament cap camp: s'està cridant al setter que Lit ha instal·lat, el qual desa el valor nou en un camp intern, i a continuació crida this.requestUpdate(...), exactament el mateix mètode de baix nivell que es va presentar a l'última lliçó del mòdul 2. Això confirma la idea que es va anticipar aleshores: les propietats reactives no són un mecanisme diferent de requestUpdate(), sinó una capa de comoditat construïda per sobre seu, que l'invoca automàticament en el moment adequat.
Aquest detall intern no cal memoritzar-lo per treballar amb Lit en el dia a dia, però conèixer-lo ajuda a raonar sobre casos menys obvis: per exemple, explica per què mutar un array o un objecte desat en una propietat reactiva (this.tareas.push(nuevaTarea)) no dispara cap actualització per si sol, mentre que reassignar la propietat completa sí que ho fa (this.tareas = [...this.tareas, nuevaTarea]): el setter només s'executa quan hi ha una assignació real a this.tareas, no quan es crida un mètode que modifica el contingut de l'array ja existent sense tornar-lo a assignar. Aquesta distinció es reprendrà amb detall quan al curs es treballi amb col·leccions que canvien amb el temps.
Errors Comuns i Consells
- Oblidar declarar una propietat a
static propertiesi esperar que sigui reactiva: si s'assignathis.algo = valorsense haver declaratalgoastatic properties(ni amb@property), aquesta assignació és un camp d'instància normal, invisible per a Lit, exactament com es va explicar al mòdul 2. El símptoma és sempre el mateix: el valor canvia internament, però la pantalla no s'actualitza. - Assignar el valor inicial dins de
static propertiesen lloc de alconstructor:static propertiesnomés declara la configuració de la propietat (tipus, atribut, reflexió); no és el lloc per posar el valor per defecte. El valor inicial s'assigna alconstructor, després desuper(), com a tots els exemples d'aquesta lliçó. - Mutar un array o un objecte reactiu esperant que dispari una actualització: com es va explicar a l'apartat 7,
this.coleccion.push(elemento)no passa pel setter de la propietat, així que Lit no ho detecta. Cal reassignar la propietat completa (per exemple, amb l'operador de propagació[...this.coleccion, elemento]) perquè el canvi sigui visible per a Lit. - Confondre nom de propietat amb nom d'atribut en propietats amb
camelCase: com es va veure a l'apartat 3, una propietatprioridadAltasense configuració explícita d'attributegenera per defecte un atributprioridadalta(tot junt), noprioridad-alta. Si es vol un atribut enkebab-case, cal indicar-ho explícitament.
Exercicis
- Afegeix a
<task-card>una nova propietat reactiva anomenadaasignadoA, de tipusString, amb valor inicial'Sin asignar', i mostra-la a la plantilla dins d'un paràgraf<p>Asignada a: ${this.asignadoA}</p>. Comprova, des de la consola del navegador, queelemento.asignadoA = 'Ana'actualitza la pantalla. - Declara a
<task-card>una propietatcompletadaEn, de tipusString, l'atribut HTML de la qual es digui explícitamentcompletada-en(utilitzant l'opcióattributevista a l'apartat 3). Escriu l'HTML d'exemple que utilitzaries per establir aquesta propietat des d'una etiqueta<task-card>. - Basant-te en l'apartat 7, explica amb les teves paraules per què
this.tareas.push(nuevaTarea)sobre una propietat reactivatareasde tipusArrayno provoca cap actualització visible, i quina línia de codi caldria escriure en el seu lloc perquè sí que la provoqués.
Solucions
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
urgente: { type: Boolean },
asignadoA: { type: String },
};
constructor() {
super();
this.titulo = 'Tarea sin título';
this.estado = 'pendiente';
this.prioridad = 3;
this.urgente = false;
this.asignadoA = 'Sin asignar';
}
render() {
return html`
<article>
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
<p>Prioridad: ${this.prioridad}</p>
<p>Asignada a: ${this.asignadoA}</p>
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
</article>
`;
}En executar elemento.asignadoA = 'Ana' des de la consola, Lit detecta el canvi a través del setter instal·lat sobre asignadoA (com es va explicar a l'apartat 7) i programa una actualització; després d'aquesta actualització, el paràgraf mostrarà "Asignada a: Ana".
static properties = {
// ...propietats anteriors...
completadaEn: { type: String, attribute: 'completada-en' },
};this.tareas.push(nuevaTarea)modifica l'array ja existent al qual apuntathis.tareas, però no executa cap assignació sobre la propietattareasen si; com que el setter instal·lat per Lit (vist a l'apartat 7) només es dispara quan hi ha una assignació real (this.tareas = ...),pushpassa completament inadvertit per a Lit i no es programa cap actualització. Perquè el canvi sigui detectat, caldria reassignar la propietat completa amb un array nou, per exemplethis.tareas = [...this.tareas, nuevaTarea];, de manera que el setter sí que s'executi.
Conclusió
En aquesta lliçó has après a declarar propietats reactives de veritat amb static properties, entenent el paper de les seves opcions de configuració (type, attribute, reflect) i la sintaxi equivalent amb el decorador @property. Has convertit <task-card> perquè titulo, estado, prioridad i urgente siguin propietats reactives reals, capaces de rebre valors tant des d'atributs HTML com des de JavaScript, i has entès, a nivell de getters i setters, per què això fa que disparin actualitzacions automàtiques on abans no passava res.
Tanmateix, no totes les propietats d'un component haurien de ser públiques d'aquesta manera. <task-card> necessitarà, a la lliçó següent, una dada interna —si la targeta està expandida o no— que no té sentit exposar com a part de la seva API pública ni com a atribut HTML. Aquest és el contingut de la lliçó següent, "Estat Intern amb @state", on coneixeràs la diferència entre una propietat pública i un estat privat del component.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
