A la lliçó anterior vas crear el teu primer component Lit sense aturar-te massa en el perquè de cada peça. Abans d'avançar cap a les plantilles reactives, convé fer una pausa i entendre amb detall l'anatomia completa d'una classe que estén LitElement: quines parts la componen, com es relaciona amb l'estàndard de Custom Elements que ja coneixes, què implica que usi Shadow DOM per defecte, i quins moments de vida travessa un component des que neix fins que desapareix. Aquesta lliçó és l'última teòrica del mòdul i tanca el cercle obert a la primera lliçó, deixant el terreny preparat per al mòdul 2, on començaràs a treballar amb plantilles realment dinàmiques.
Contingut
- Estructura general d'una classe LitElement
- Declarar propietats: decoradors davant de
static properties - LitElement és HTMLElement: la relació amb l'estàndard
- Shadow DOM per defecte: què significa i què implica
- Panorama del cicle de vida d'un component Lit
- Cap al mòdul 2: plantilles reactives
- Estructura general d'una classe LitElement
Tota classe que estén LitElement pot contenir, en general, els blocs següents (cap és obligatori excepte render(), que és l'únic imprescindible perquè el component mostri alguna cosa):
import { LitElement, html, css } from 'lit';
class TaskCard extends LitElement {
// 1. Declaració de propietats reactives (es detalla al mòdul 3)
static properties = {
titulo: { type: String },
};
// 2. Estils encapsulats (es detallen al mòdul 4)
static styles = css`
:host {
display: block;
border: 1px solid #ccc;
}
`;
// 3. Constructor: inicialització de l'estat intern
constructor() {
super();
this.titulo = 'Tarea sin título';
}
// 4. Callbacks del cicle de vida (es detallen al mòdul 6)
connectedCallback() {
super.connectedCallback();
}
// 5. Plantilla del component (obligatori)
render() {
return html`<h3>${this.titulo}</h3>`;
}
}
customElements.define('task-card', TaskCard);No et preocupis si algunes d'aquestes peces (static properties, static styles, connectedCallback) encara no tenen gaire sentit: s'explicaran amb profunditat als mòduls 3, 4 i 6 respectivament. L'objectiu d'aquest repàs és que, en arribar a aquests mòduls, ja tinguis clara la "forma" general de la classe i sàpigues ubicar cada peça nova dins d'un esquema conegut. De fet, fixa't que el component task-card.js de la lliçó anterior era, simplement, una versió d'aquesta mateixa estructura en la qual només s'omplia el bloc 5 (render()); tots els altres blocs són opcionals i s'afegeixen segons les necessitats del component.
Un detall sobre el constructor: si es defineix, és obligatori cridar primer a super(), com en qualsevol classe de JavaScript que hereta d'una altra. És el lloc habitual per inicialitzar valors per defecte de les propietats del component, cosa que es reprendrà amb més detall al mòdul 3.
- Declarar propietats: decoradors davant de
static properties
static propertiesA la lliçó anterior ja vas veure que existeixen dos estils per registrar un component: customElements.define i el decorador @customElement. Aquesta mateixa dualitat d'estils es repeteix per declarar les propietats reactives d'un component (el seu estudi complet correspon al mòdul 3, però convé reconèixer ja la sintaxi, perquè apareixerà en exemples d'aquest curs i a la documentació oficial de Lit).
Amb decoradors (requereix suport de compilació, com es va explicar a la lliçó de configuració de l'entorn):
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';
render() {
return html`<h3>${this.titulo}</h3>`;
}
}Amb static properties, en JavaScript pla sense cap pas de compilació addicional:
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
};
constructor() {
super();
this.titulo = 'Tarea sin título';
}
render() {
return html`<h3>${this.titulo}</h3>`;
}
}
customElements.define('task-card', TaskCard);| Aspecte | Amb decoradors (@property) |
Amb static properties |
|---|---|---|
| Requereix compilació addicional | Sí (TypeScript o Babel amb plugin de decoradors) | No, JavaScript estàndard |
| On es declara el valor per defecte | Junt a la mateixa declaració de la propietat | Al constructor, després de super() |
| Verbositat | Més compacta | Lleugerament més extensa |
| Resultat en temps d'execució | Idèntic | Idèntic |
Tots dos estils produeixen exactament el mateix comportament; la tria és una qüestió de preferència i de les eines disponibles al projecte. Aquest curs, com ja es va explicar a la lliçó de configuració de l'entorn, usarà principalment static properties, mostrant la variant amb decoradors com a referència quan resulti útil per a qui treballi amb TypeScript en els seus propis projectes.
- LitElement és HTMLElement: la relació amb l'estàndard
És fonamental interioritzar una idea que ja es va apuntar a la primera lliçó del mòdul: LitElement no substitueix l'estàndard de Custom Elements, l'estén. En termes de JavaScript, la cadena d'herència és:
HTMLElementés la classe base de la qual hereten tots els elements del DOM, tant els natius del navegador (HTMLButtonElement,HTMLDivElement...) com qualsevol Custom Element.ReactiveElementés una classe intermèdia que Lit afegeix per incorporar el sistema de propietats reactives i el cicle d'actualització, sense encara imposar un motor concret de plantilles.LitElementhereta deReactiveElementi afegeix el mètoderender()juntament amb l'ús de la funcióhtmlcom a motor de plantilles.- La teva classe (
TaskCard, en l'exemple) hereta deLitElementi hereta, per tant, totes les capacitats de les tres classes anteriors.
La conseqüència pràctica d'aquesta cadena d'herència és que un component Lit és, literalment, un HTMLElement. Això significa que hereta tota l'API estàndard d'un element del DOM: se li poden afegir i treure atributs (setAttribute, getAttribute), se li poden escoltar i despatxar esdeveniments (addEventListener, dispatchEvent, cosa que s'explota a fons al mòdul d'esdeveniments), té accés a classList, i participa a l'arbre del DOM exactament igual que qualsevol altre element. Res d'això és una simulació ni una capa a part: és la mateixa plataforma web.
- Shadow DOM per defecte: què significa i què implica
A la lliçó anterior, en inspeccionar <task-card> a les eines de desenvolupament, va aparèixer un node #shadow-root que potser va resultar sorprenent. Això passa perquè LitElement, per defecte, crida internament a attachShadow({ mode: 'open' }) sobre cada instància del component i renderitza el resultat de render() dins d'aquest Shadow DOM, no directament com a fills de l'element al DOM normal.
// Això és, de manera simplificada, el que fa LitElement per tu
// la primera vegada que el component necessita renderitzar-se:
const raizSombra = this.attachShadow({ mode: 'open' });
raizSombra.innerHTML = /* resultat de render() */;Què implica això a la pràctica, sense entrar encara en el detall dels estils (que correspon al mòdul 4)?
- Encapsulació de l'HTML intern: el contingut retornat per
render()no és visible mitjançantdocument.querySelectordes de fora del component; cal accedir primer aelemento.shadowRooti buscar dins seu. - Encapsulació dels estils: qualsevol CSS que es declari dins del component (amb
static styles, com es va veure a l'apartat 1) no es filtra cap a la resta de la pàgina, i els estils globals de la pàgina tampoc no entren per accident al component. Això evita un dels problemes més habituals en construir interfícies grans: col·lisions de noms de classes CSS entre components diferents. - Contingut distribuït amb
<slot>: el Shadow DOM també defineix un mecanisme,<slot>, per permetre que un component rebi contingut HTML "de fora" i el posicioni dins de la seva pròpia estructura interna. Aquest mecanisme s'explorarà amb detall al mòdul d'estils.
Per ara, el punt clau que cal retenir és aquest: cada component Lit viu, per defecte, en la seva pròpia bombolla aïllada de la resta de la pàgina, tant en estructura com en estils. Aquesta és una de les raons per les quals Lit resulta especialment adequat per construir biblioteques de components reutilitzables com els de TaskFlow: es poden combinar <task-card>, <task-list> i <user-avatar> sense por a que els estils d'un interfereixin amb els d'un altre.
- Panorama del cicle de vida d'un component Lit
Un component Lit, igual que qualsevol Custom Element, no existeix de manera estàtica: neix, s'actualitza possiblement moltes vegades, i en algun moment pot desaparèixer del DOM. Lit ofereix una sèrie de mètodes ("callbacks del cicle de vida") que s'executen automàticament en diferents moments d'aquesta vida, i que un component pot sobreescriure per executar lògica pròpia en el moment adequat.
En aquesta lliçó només es presenta un mapa general, amb el nom de cada callback i per a què serveix a grans trets; el funcionament detallat, amb exemples d'ús real, s'estudia amb profunditat al mòdul 6 ("Cicle de Vida i Comportament Avançat").
| Moment del cicle de vida | Callback | Per a què serveix, a grans trets |
|---|---|---|
| L'element s'insereix al DOM | connectedCallback() |
Heretat directament de l'estàndard de Custom Elements; bon lloc per subscriure's a esdeveniments externs o iniciar temporitzadors |
| L'element es retira del DOM | disconnectedCallback() |
També de l'estàndard; lloc habitual per "netejar" allò que es va iniciar a connectedCallback (cancel·lar subscripcions, temporitzadors...) |
| Abans d'aplicar una actualització | willUpdate(changedProperties) |
Propi de Lit; permet calcular valors derivats just abans de renderitzar, en funció de quines propietats han canviat |
| Es genera la nova plantilla | render() |
L'únic obligatori; retorna l'HTML actual del component |
| Després d'aplicar una actualització al DOM | updated(changedProperties) |
Propi de Lit; útil per reaccionar a canvis ja reflectits al DOM real |
| Just després de la primera actualització | firstUpdated(changedProperties) |
Propi de Lit; s'executa una única vegada, ideal per a inicialitzacions que necessiten que el DOM intern ja existeixi |
Dues idees importants a retenir, encara que el detall arribi més endavant:
- Els callbacks
connectedCallbackidisconnectedCallbackno són una invenció de Lit: pertanyen a l'estàndard de Custom Elements vist a la primera lliçó del mòdul. Lit els aprofita i afegeix la seva pròpia capa de callbacks addicionals (willUpdate,updated,firstUpdated) específics del seu sistema de renderitzat reactiu. - Cap component necessita implementar tots aquests callbacks. La majoria dels components senzills, com el
<task-card>estàtic de la lliçó anterior, no necessiten cap d'ells; es van incorporant a mesura que el component necessita reaccionar a moments concrets de la seva vida.
- Cap al mòdul 2: plantilles reactives
Amb aquesta lliçó es tanca el mòdul d'introducció. Ja coneixes la base sobre la qual es recolza tota la resta: què són els Web Components com a estàndard, què aporta Lit sobre ells, com configurar un entorn de treball, com es veu per dins una classe LitElement, i quins moments de vida travessa un component.
Tot i això, tots els exemples vistos fins ara comparteixen una limitació deliberada: les seves plantilles són completament estàtiques. El <task-card> que has construït sempre mostra la mateixa tasca d'exemple, sense importar què passi a l'aplicació. Aquesta limitació és exactament el punt de partida del mòdul 2, "Plantilles Reactives i Renderitzat", on aprendràs a inserir expressions dinàmiques dins de la funció html, a mostrar o ocultar contingut segons condicions, a renderitzar llistes completes de tasques, i a entendre com i quan decideix Lit tornar a executar render().
Errors Comuns i Consells
- Intentar accedir al contingut intern del component amb
document.querySelectordes de fora: com que el contingut viu dins d'un Shadow DOM,document.querySelector('task-card h3')no trobarà res. Cal accedir primer al propi element i després al seushadowRoot(elemento.shadowRoot.querySelector('h3')), o millor encara, evitar manipular el DOM intern manualment i deixar que siguirender()qui decideixi què es mostra. - Oblidar cridar a
super()al constructor: si es defineix unconstructorpersonalitzat sense invocar primer asuper(), el navegador llançarà un error en temps d'execució. Aquesta regla no és específica de Lit, sinó de qualsevol classe de JavaScript que hereti d'una altra. - Pensar que cal implementar tots els callbacks del cicle de vida: com s'ha vist a la taula de l'apartat 5, la majoria dels components només necessiten
render(). Afegir callbacks "per si de cas" complica el component sense necessitat. - Confondre
ReactiveElementambLitElement: en el dia a dia gairebé mai s'interactua directament ambReactiveElement; es menciona aquí només per entendre la cadena d'herència. A la pràctica, tots els components d'aquest curs estendranLitElementdirectament.
Exercicis
- Dibuixa (en un paper o en un fitxer de text) la cadena d'herència completa d'un component
UserAvatarque definiràs tu mateix, des deHTMLElementfins a la teva classe, indicant què aporta cada esglaó de la cadena. - Recupera el component
task-card.jsde la lliçó anterior (la versió estàtica) i afegeix un mètodeconnectedCallback()que, mitjançantconsole.log, escrigui a la consola del navegador el missatge "task-card conectado al DOM". Recorda cridar asuper.connectedCallback(). - Explica amb les teves pròpies paraules, en dues o tres frases, per què el fet que
LitElementusi Shadow DOM per defecte és un avantatge per a un projecte com TaskFlow, que tindrà diversos components diferents convivint a la mateixa pàgina.
Solucions
- La cadena, igual que la de
TaskCardvista a l'apartat 3, és:
HTMLElement (estàndard del navegador: API base de qualsevol element del DOM)
→ ReactiveElement (Lit: sistema de propietats reactives i cicle d'actualització)
→ LitElement (Lit: motor de plantilles mitjançant render() i html)
→ UserAvatar (la teva classe: la lògica i plantilla concretes de l'avatar)import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
connectedCallback() {
super.connectedCallback();
console.log('task-card conectado al DOM');
}
render() {
return html`
<article>
<h3>Preparar la demo del sprint</h3>
<p>Estado: En curso</p>
<p>Asignada a: Ana</p>
</article>
`;
}
}
customElements.define('task-card', TaskCard);En recarregar la pàgina amb les eines de desenvolupament obertes, hauria d'aparèixer el missatge a la consola per cada <task-card> present a l'HTML.
- Una possible resposta: en estar cada component encapsulat en el seu propi Shadow DOM, els estils CSS de
<task-card>,<task-list>o<user-avatar>no poden interferir entre si ni amb els estils globals de la pàgina, encara que usin els mateixos noms de classe o d'etiqueta. Això permet desenvolupar i mantenir cada component de manera independent, sense necessitat de coordinar convencions de noms CSS entre tot l'equip, cosa especialment valuosa a mesura que TaskFlow vagi sumant més components al llarg del curs.
Conclusió
En aquesta lliçó has acabat de desmuntar l'anatomia d'un component Lit: la seva estructura general amb blocs opcionals i un únic mètode obligatori (render()), les dues formes de declarar propietats (decoradors i static properties), la seva relació directa amb HTMLElement a través de la cadena d'herència HTMLElement → ReactiveElement → LitElement, l'ús del Shadow DOM per defecte per encapsular estructura i estils, i una primera panoràmica —només els noms i el seu propòsit general— del cicle de vida que es detallarà al mòdul 6.
Amb això es tanca el mòdul d'introducció. Al mòdul 2, "Plantilles Reactives i Renderitzat", deixaràs enrere els components estàtics: aprendràs a inserir expressions dinàmiques a la funció html, a renderitzar condicionalment contingut, a mostrar llistes d'elements i a entendre en profunditat el cicle de renderitzat de Lit, donant a <task-card>, <task-list> i la resta de peces de TaskFlow la seva primera capa real de vida dinàmica.
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
