A la lliçó anterior <task-card> va guanyar quatre propietats reactives públiques: titulo, estado, prioridad i urgente. Les quatre tenen alguna cosa en comú: formen part de les dades de la tasca, vénen "de fora" (de qui utilitzi el component) i té sentit que es puguin establir com a atributs HTML. Però no tot el que necessita reactivitat dins d'un component encaixa en aquest perfil. Aquesta lliçó presenta l'estat intern privat, declarat amb state: true (o el decorador @state), i l'utilitza per afegir a <task-card> la capacitat d'expandir-se i mostrar més detall en fer clic, sense que aquest detall expandit formi part de l'API pública del component.
Continguts
- Dos tipus de dada reactiva: propietat pública i estat intern
- Declarar estat intern amb
state: true - El decorador
@statecom a alternativa - Casos d'ús típics de l'estat intern
- Afegint
expandidaa<task-card> - Una menció de passada: la gestió del clic
- Dos tipus de dada reactiva: propietat pública i estat intern
Les propietats vistes a la lliçó anterior (titulo, estado, prioridad, urgente) comparteixen una característica de fons: formen part de l'API pública del component. Són dades que un altre codi —el component pare, o qui escrigui l'HTML on s'utilitza <task-card>— necessita poder establir des de fora, ja sigui mitjançant un atribut HTML o mitjançant una assignació de JavaScript. Per això té sentit que Lit els creï, per defecte, un atribut HTML associat, com es va explicar a l'apartat 3 de la lliçó anterior.
Però un component, a mesura que guanya comportament propi, sovint necessita desar dades que no tenen aquest perfil: dades que només li importen a ell mateix, per gestionar el seu propi comportament intern, i que ningú de fora del component hauria ni de necessitar llegir ni, sobretot, de necessitar establir. Un exemple típic, que és justament el que es construirà en aquesta lliçó: si una targeta de tasca està "expandida" (mostrant més detall) o "contreta" (mostrant només el resum). Aquesta dada no descriu la tasca en si —una tasca no "és" expandida o contreta, és la targeta que la representa la que té aquest estat visual—, i no té cap sentit que algú escrigui <task-card expandida="true"> al seu HTML esperant controlar aquest detall des de fora.
Per a aquest segon tipus de dada, Lit ofereix l'estat intern (en anglès, internal reactive state), que a static properties es declara amb l'opció state: true en lloc de type. Un estat intern continua sent completament reactiu —canviar el seu valor dispara una actualització exactament igual que amb una propietat pública—, però no genera cap atribut HTML, i Lit el marca, tant a la seva documentació com a eines de desenvolupament, com una peça que pertany a la implementació interna del component, no a la seva interfície pública.
| Aspecte | Propietat pública (type: String, etc.) |
Estat intern (state: true) |
|---|---|---|
| És reactiva? | Sí | Sí |
| Genera atribut HTML? | Sí, per defecte | No, mai |
| Forma part de l'API del component? | Sí | No |
| Qui l'estableix habitualment? | El codi que utilitza el component (pare, HTML) | El propi component, des de dins |
Exemple a <task-card> |
titulo, estado, prioridad, urgente |
expandida |
- Declarar estat intern amb
state: true
state: trueLa sintaxi és una variació mínima del que ja s'ha vist a la lliçó anterior: en lloc de { type: Boolean }, s'escriu { state: true }.
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
expandida: { state: true },
};
constructor() {
super();
this.titulo = 'Tarea sin título';
this.expandida = false;
}
render() {
return html`
<article>
<h3>${this.titulo}</h3>
${this.expandida ? html`<p>Detalle adicional de la tarea...</p>` : ''}
</article>
`;
}
}Fixa't que expandida es continua assignant al constructor, igual que qualsevol altra propietat reactiva, i es continua llegint a render() amb this.expandida, exactament igual que titulo. L'única diferència real és a la declaració: { state: true } en lloc de { type: Boolean }. Aquest petit canvi en la declaració n'hi ha prou per fer que Lit no generi cap atribut expandida a l'HTML de l'element, encara que la propietat JavaScript this.expandida continuï existint i continuï essent perfectament reactiva.
Una convenció habitual, encara que no obligatòria, en el codi Lit real és posar un guió baix davant del nom dels camps d'estat intern més delicats, o almenys evitar qualsevol documentació pública que convidi a llegir-los o escriure'ls des de fora del component; Lit no imposa cap restricció tècnica que impedeixi accedir a elemento.expandida des de fora (JavaScript no té un veritable mecanisme de "privat" per a camps declarats així, més enllà dels camps amb # del propi llenguatge, que Lit no utilitza aquí), però la intenció de disseny és clara: és un detall intern, i tractar-lo com a tal a la resta del codi evita acoblar el comportament extern del component a una dada que podria canviar de nom o de forma sense avís previ.
- El decorador
@state com a alternativa
@state com a alternativaIgual que amb @property a la lliçó anterior, hi ha una sintaxi equivalent amb decoradors per a l'estat intern:
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
@customElement('task-card')
class TaskCard extends LitElement {
@property({ type: String })
titulo = 'Tarea sin título';
@state()
expandida = false;
render() {
return html`
<article>
<h3>${this.titulo}</h3>
${this.expandida ? html`<p>Detalle adicional de la tarea...</p>` : ''}
</article>
`;
}
}El decorador @state() no admet les mateixes opcions que @property() (no tindria sentit passar-li type o attribute, ja que un estat intern mai té atribut associat); simplement marca el camp com a estat reactiu intern. El resultat en temps d'execució és idèntic al de { state: true } a static properties.
- Casos d'ús típics de l'estat intern
Abans d'aplicar expandida a <task-card>, convé tenir clar el patró general de quan utilitzar estat intern en lloc d'una propietat pública. Alguns exemples típics, més enllà del que es construirà en aquesta lliçó:
- Un comptador intern: per exemple, quantes vegades s'ha polsat un botó dins del propi component, sense que aquest recompte formi part de les dades que descriu el component cap enfora.
- Un flag d'"expandit/replegat": exactament el cas de
<task-card>en aquesta lliçó; un<details>personalitzat, un menú desplegable, o qualsevol component amb una secció que es mostra o s'amaga segons una interacció del propi component. - L'estat d'una petició en curs: un camp com
cargando(true/false) mentre un component espera una resposta de xarxa, que només li interessa a ell mateix per decidir què mostrar mentrestant (un text "Cargando..." o un indicador visual). - Un valor intermedi calculat a partir de propietats públiques: per exemple, si un component rep una llista llarga com a propietat pública però internament només mostra una pàgina de resultats a la vegada, el número de pàgina actual seria un bon candidat a estat intern.
La pregunta que convé fer-se sempre per decidir entre propietat pública i estat intern és: té sentit que algú, des de fora del component, vulgui llegir o establir aquest valor com a part de com utilitza el component? Si la resposta és sí, és una propietat pública. Si el valor només té sentit com a detall d'implementació del propi component, és estat intern.
- Afegint
expandida a <task-card>
expandida a <task-card>Amb el criteri de l'apartat anterior, expandida encaixa clarament com a estat intern: descriu si la targeta, com a peça d'interfície, està mostrant el seu detall ampliat, no una dada de la tasca en si. Recupera src/components/task-card.js tal com va quedar a la lliçó anterior i afegeix el nou estat:
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
urgente: { type: Boolean },
expandida: { state: true },
};
constructor() {
super();
this.titulo = 'Tarea sin título';
this.estado = 'pendiente';
this.prioridad = 3;
this.urgente = false;
this.expandida = false;
}
alternarExpandida() {
this.expandida = !this.expandida;
}
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 @click="${this.alternarExpandida}">
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
<p>Prioridad: ${this.prioridad}</p>
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
${this.expandida
? html`
<div class="detalle">
<p>Estado interno: la tarjeta está expandida.</p>
<p>Aquí, más adelante en el curso, podría mostrarse una descripción larga, un histórico de cambios, o comentarios de la tarea.</p>
</div>
`
: ''}
</article>
`;
}
}
customElements.define('task-card', TaskCard);El mètode alternarExpandida() inverteix el valor booleà de this.expandida amb l'operador de negació (!this.expandida); com que expandida és un estat intern reactiu, aquesta assignació passa pel mateix mecanisme de setter explicat a la lliçó anterior i dispara una actualització, exactament igual que amb qualsevol propietat pública. La plantilla utilitza l'operador ternari vist al mòdul 2 per mostrar o amagar el bloc de detall segons el valor de this.expandida.
- Una menció de passada: la gestió del clic
És possible que t'hagi cridat l'atenció la sintaxi @click="${this.alternarExpandida}" dins de l'etiqueta <article> de l'exemple anterior: és la manera en què Lit permet escoltar esdeveniments del DOM directament des d'una plantilla, amb un prefix @ seguit del nom de l'esdeveniment i, entre cometes, una referència a la funció que s'ha d'executar en produir-se aquest esdeveniment (en aquest cas, un esdeveniment click del navegador sobre el propi <article>).
Aquest curs tracta els esdeveniments en profunditat al mòdul 5, "Esdeveniments i Comunicació entre Components", on s'explicarà aquesta sintaxi amb detall, com despatxar esdeveniments personalitzats propis d'un component (per exemple, perquè <task-card> avisi <task-list> que l'usuari vol marcar una tasca com a completada), i com gestionar correctament el valor de this dins del mètode gestor. Aquí només s'utilitza el mínim indispensable —un esdeveniment natiu del navegador (click) sobre un mètode que ja no rep cap argument del propi esdeveniment— per poder disparar el canvi d'expandida i així completar l'exemple de l'estat intern; no cal aprofundir més en aquest punt del curs.
L'important per a aquesta lliçó és constatar que, sigui quin sigui l'origen del canvi (un clic de l'usuari, una resposta de xarxa, un temporitzador...), el mecanisme de reactivitat és exactament el mateix que ja coneixes: s'assigna un valor nou a una propietat o a un estat reactiu, i Lit s'encarrega de programar l'actualització corresponent.
Errors Comuns i Consells
- Declarar com a propietat pública una dada que en realitat és estat intern: si es declara
expandidaamb{ type: Boolean }en lloc de{ state: true }, el component continuarà funcionant (la reactivitat és la mateixa), però es generarà innecessàriament un atribut HTMLexpandidaque forma part, sense voler-ho, de l'API pública del component, convidant a un ús que no té sentit (<task-card expandida></task-card>). - Esperar que
state: trueimpedeixi l'accés des de fora: com es va explicar a l'apartat 2,state: trueés una convenció de disseny recolzada per Lit (sense atribut, marcada com a interna a eines de desenvolupament), però no un mecanisme de privacitat real de JavaScript. Res impedeix tècnicament que codi extern llegeixi o escriguielemento.expandida; simplement no es documenta ni s'espera que s'utilitzi així. - Oblidar inicialitzar l'estat intern al
constructor: exactament el mateix error que amb les propietats públiques del mòdul anterior; si no s'assigna un valor inicial athis.expandidaalconstructor, el seu valor seràundefinedal primer renderitzat, la qual cosa pot produir comportaments inesperats a les expressions condicionals de la plantilla. - Ficar massa lògica de negoci a l'estat intern d'un component de presentació: un component com
<task-card>pot tenir perfectament el seu propi estat visual (expandida), però si comença a acumular estat intern relacionat amb dades de negoci (per exemple, una còpia pròpia de la llista completa de tasques), és un senyal de que aquesta responsabilitat probablement hauria de viure en un component pare i arribar com a propietat pública, no duplicar-se com a estat intern.
Exercicis
- Afegeix a
<task-card>un nou estat internvecesExpandida, de tipus numèric, inicialitzat a0, que s'incrementi en un cada vegada que es cridaalternarExpandida()(independentment de si el resultat és expandir o contreure). Mostra'l a la plantilla només amb finalitats de depuració, per exemple com a<p>Expandida ${this.vecesExpandida} veces</p>. - Explica, recolzant-te en la taula de l'apartat 1, per què no tindria sentit declarar
titulocom a{ state: true }en lloc de{ type: String }. - Imagina un component
<user-avatar>(un dels components previstos per a TaskFlow més endavant al curs) que mostra la imatge d'un usuari i, mentre aquesta imatge encara no ha acabat de carregar, un color de fons de farciment. Decideix, raonant amb el criteri de l'apartat 4, si la dada "la imatge ja ha acabat de carregar" (true/false) hauria de ser una propietat pública o un estat intern.
Solucions
static properties = {
// ...propietats anteriors...
expandida: { state: true },
vecesExpandida: { state: true },
};
constructor() {
super();
// ...
this.expandida = false;
this.vecesExpandida = 0;
}
alternarExpandida() {
this.expandida = !this.expandida;
this.vecesExpandida = this.vecesExpandida + 1;
}Com que ambdues assignacions passen dins de la mateixa funció síncrona, Lit les agrupa (segons el que es va explicar a la lliçó del cicle de renderitzat del mòdul 2) i executa render() una sola vegada amb els dos valors ja actualitzats.
-
tituloés una dada que descriu la mateixa tasca i que necessita poder-se establir des de fora del component: qui utilitzi<task-card>(ja sigui escrivint l'atribut HTML directament o assignant la propietat des de<task-list>) necessita indicar quin títol mostrar. Si es declarés com a estat intern (state: true), Lit no generaria cap atribut HTML associat, i seria impossible establir el títol mitjançant<task-card titulo="...">; només quedaria la via d'assignar-lo per JavaScript després de crear l'element, la qual cosa limita innecessàriament com es pot utilitzar el component. -
És un bon candidat a estat intern: el fet que la imatge concreta ja hagi acabat de carregar és un detall d'implementació de com
<user-avatar>gestiona la seva pròpia càrrega de recursos, no una dada que descrigui l'usuari ni res que qui utilitzi el component necessiti establir des de fora (ningú escriuria<user-avatar imagen-cargada="true">esperant controlar aquest aspecte). Encaixa amb el criteri de l'apartat 4: només li importa al propi component, per decidir internament què mostrar mentrestant.
Conclusió
En aquesta lliçó has après a distingir entre propietats públiques, que formen part de l'API d'un component i solen tenir un atribut HTML associat, i estat intern privat, declarat amb state: true (o @state), reactiu igual que qualsevol propietat però sense generar cap atribut i sense pertànyer a la interfície externa del component. Has aplicat aquesta distinció a <task-card>, que ara pot expandir-se i contreure's mitjançant un estat intern expandida, alternat amb un gestor de clic mínim el detall complet del qual es reserva per al mòdul 5.
Fins ara, totes les propietats declarades —tant públiques com internes— han utilitzat els tipus més senzills: String, Number i Boolean. A la lliçó següent, "Tipus de Propietats i Conversors Personalitzats", veuràs el catàleg complet de tipus suportats de sèrie per Lit, entendràs amb detall com converteix entre el text d'un atribut HTML i el valor JavaScript de la propietat, i aprendràs a definir un conversor propi per a un tipus de dada que Lit no sap interpretar de manera nativa, aplicant-lo a una nova propietat fechaLimite a <task-card>.
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
