Ha arribat el moment d'escriure codi de veritat. En aquesta lliçó crearàs el teu primer component amb Lit, començant pel clàssic "Hola Món" i avançant de seguida cap a alguna cosa amb més sentit dins d'aquest curs: la primera versió de <task-card>, el component que representarà una tasca individual a TaskFlow. Aquesta primera versió serà deliberadament senzilla —sense propietats ni dades dinàmiques— per centrar-se a entendre la mecànica bàsica d'un component Lit: com es defineix la classe, com es declara la seva plantilla i com es registra com una etiqueta HTML utilitzable.
Contingut
- Els tres passos de tot component Lit
- Hola Món amb LitElement
- Registrar el component:
customElements.definei@customElement - Usar el component com una etiqueta HTML normal
- Primera versió de
<task-card> - Veure-ho funcionar a la pàgina
- Els tres passos de tot component Lit
Tot component fet amb Lit, per complex que arribi a ser més endavant en el curs, es construeix sempre seguint els mateixos tres passos:
- Estendre
LitElement: es crea una classe de JavaScript que hereta deLitElement, la classe base que proporciona Lit. - Implementar
render(): dins d'aquesta classe es defineix un mètoderender()que retorna la plantilla HTML del component, fent servir la funcióhtml. - Registrar l'element: se li dona un nom d'etiqueta al component mitjançant
customElements.define(o el decorador equivalent@customElement), perquè el navegador sàpiga quina classe ha d'usar quan trobi aquesta etiqueta a l'HTML.
La resta d'aquesta lliçó desenvolupa cadascun d'aquests tres passos amb exemples concrets.
- Hola Món amb LitElement
Comencem per l'exemple més senzill possible. Dins del projecte taskflow creat a la lliçó anterior, crea un fitxer src/components/hola-mundo.js amb el contingut següent:
import { LitElement, html } from 'lit';
class HolaMundo extends LitElement {
render() {
return html`<p>¡Hola Mundo desde Lit!</p>`;
}
}
customElements.define('hola-mundo', HolaMundo);Analitzem cada línia:
import { LitElement, html } from 'lit';: s'importen les dues peces que es faran servir.LitElementés la classe base de la qual hereta qualsevol component Lit;htmlés una funció especial (una tagged template literal, una característica estàndard de JavaScript) que permet escriure HTML de manera llegible dins del codi JavaScript.class HolaMundo extends LitElement { ... }: es declara una classe normal de JavaScript que hereta deLitElement. A partir d'aquí,HolaMundoja té totes les capacitats que ofereix Lit (renderitzat reactiu, cicle de vida, etc.), encara que en aquest primer exemple no se n'usi cap de les més avançades.render() { return html\¡Hola Mundo desde Lit!
`; }: el mètoderender()és el cor de qualsevol component Lit. Lit l'invoca automàticament quan el component necessita mostrar-se (o actualitzar-se) i espera que retorni el resultat de la funcióhtml`. Aquí, simplement es retorna un paràgraf amb un text fix.customElements.define('hola-mundo', HolaMundo);: aquesta línia registra la classeHolaMundocom la implementació de l'etiqueta<hola-mundo>. A partir d'aquest moment, qualsevol<hola-mundo>que aparegui a l'HTML de la pàgina serà "activada" pel navegador fent servir aquesta classe.
Un detall important sobre la funció html: encara que a simple vista sembli una simple cadena de text, no ho és. html és una tagged template literal que Lit analitza de manera especial, la qual cosa li permet, entre altres coses, saber exactament quines parts de la plantilla són fixes i quines són dinàmiques (això s'explota a fons al mòdul de plantilles reactives). Per ara, amb una plantilla completament estàtica com aquesta, n'hi ha prou de saber que el contingut entre els accents greus (`) és l'HTML que es mostrarà.
- Registrar el component:
customElements.define i @customElement
customElements.define i @customElementLa crida a customElements.define('hola-mundo', HolaMundo) no és una particularitat de Lit: és la manera estàndard en què l'especificació de Custom Elements (vista a la lliçó anterior) registra qualsevol element personalitzat, amb o sense Lit pel mig. Lit no substitueix aquest mecanisme, simplement l'usa.
Existeix una manera alternativa d'escriure aquest registre, mitjançant un decorador, si el projecte té suport per a decoradors (com és el cas de la plantilla de Lit de Vite vista a la lliçó anterior):
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('hola-mundo')
class HolaMundo extends LitElement {
render() {
return html`<p>¡Hola Mundo desde Lit!</p>`;
}
}Totes dues formes són equivalents: @customElement('hola-mundo') és simplement una manera més concisa d'escriure, just a sobre de la classe, el mateix que customElements.define('hola-mundo', HolaMundo) fa després d'ella. La taula següent resumeix quan convé cada estil:
| Estil | Avantatge | Quan usar-lo |
|---|---|---|
customElements.define(...) |
No requereix cap configuració especial de compilació; funciona en JavaScript pur sense passos addicionals | Projectes en JavaScript pla, sense Babel/TypeScript configurat per a decoradors |
@customElement(...) |
Més concís i llegible; manté el nom de l'etiqueta enganxat a la definició de la classe | Projectes en TypeScript, o en JavaScript amb Babel configurat per suportar decoradors (com la plantilla de Vite) |
Un requisit important, comú als dos estils i heretat directament de l'estàndard de Custom Elements: el nom de l'etiqueta ha de contenir sempre almenys un guionet (hola-mundo, task-card, user-avatar...). Aquesta regla existeix per garantir que les etiquetes personalitzades mai xoquin amb les etiquetes natives de l'HTML actuals o futures, que mai no porten guionet.
Aquest curs usarà majoritàriament customElements.define, per ser la forma que funciona sense cap configuració addicional, mostrant @customElement com a alternativa quan aporti claredat.
- Usar el component com una etiqueta HTML normal
Un cop registrat, <hola-mundo> s'usa exactament igual que qualsevol etiqueta nativa d'HTML. No cal "muntar-lo" mitjançant una funció especial ni passar-li res a través de JavaScript perquè aparegui: n'hi ha prou d'escriure'l a l'HTML.
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>TaskFlow</title> <script type="module" src="/src/components/hola-mundo.js"></script> </head> <body> <h1>Mi primera prueba con Lit</h1> <hola-mundo></hola-mundo> </body> </html>
Dos detalls a tenir en compte:
- El
<script>que carrega el component ha de tenirtype="module", perquè el fitxer usaimport, una característica dels mòduls ES estàndard de JavaScript. - L'element
<hola-mundo></hola-mundo>es pot col·locar a qualsevol part del<body>, tantes vegades com es vulgui, igual que es faria amb un<div>o un<button>. Cada aparició de l'etiqueta crea una instància independent del component.
- Primera versió de
<task-card>
<task-card>Amb la mecànica bàsica ja clara, és el moment de crear el primer component real de TaskFlow: <task-card>. En aquesta lliçó serà una versió deliberadament senzilla i totalment estàtica: mostrarà sempre el mateix contingut d'exemple, sense cap propietat configurable ni cap dada dinàmica. La capacitat de personalitzar cada targeta amb dades reals (títol, estat, persona assignada...) arribarà amb les propietats reactives, al mòdul 3.
Crea el fitxer src/components/task-card.js:
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
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);Aquest component segueix exactament els mateixos tres passos vistos a l'apartat 1: hereta de LitElement, implementa render() retornant una plantilla html (en aquest cas amb diverses etiquetes anidades: un <article> que embolcalla un títol i dos paràgrafs) i es registra amb customElements.define sota el nom task-card.
Per usar-lo, es pot crear una petita pàgina de prova, index.html, a l'arrel del projecte (si uses l'estructura de Vite, substitueix el contingut d'exemple que porta per defecte):
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>TaskFlow</title> <script type="module" src="/src/components/task-card.js"></script> </head> <body> <h1>TaskFlow</h1> <task-card></task-card> </body> </html>
- Veure-ho funcionar a la pàgina
Amb el servidor de desenvolupament arrencat (npm run dev, com es va explicar a la lliçó anterior) i el navegador obert a l'URL local, hauries de veure la targeta renderitzada a la pàgina. Un exercici útil en aquest punt és obrir les eines de desenvolupament del navegador (F12) i inspeccionar l'element <task-card>: si Shadow DOM no estigués actiu (encara no l'hem usat explícitament, ja que no s'ha cridat attachShadow ni s'ha usat la plantilla dins d'un render amb Shadow DOM propi de Lit), veuries el contingut de l'<article> com a fills directes de l'element al DOM normal ("light DOM").
És important aclarir un matís aquí: encara que en aquest exemple no s'ha esmentat explícitament, LitElement sí usa Shadow DOM per defecte per renderitzar el contingut retornat per render(). És a dir, en inspeccionar <task-card> a les eines de desenvolupament, en realitat veuràs una etiqueta especial #shadow-root dins de l'element, i l'<article> penjant d'ella, no directament del <task-card>. Això és exactament el que s'espera: és el comportament per defecte de qualsevol component Lit. El següent tema d'aquest mateix mòdul explica amb més detall què implica això i per què és així; de moment, si veus aquest #shadow-root a l'inspector, és la confirmació de que tot funciona correctament.
Errors Comuns i Consells
- Oblidar
type="module"a l'<script>: si el component no apareix i la consola del navegador mostra un error relacionat ambimport, el primer que cal comprovar és que l'<script>que carrega el fitxer té l'atributtype="module". - Nom d'etiqueta sense guionet:
customElements.define('taskcard', TaskCard)llançarà un error en temps d'execució, perquè l'estàndard exigeix almenys un guionet al nom. La forma correcta éstask-card. - Registrar el mateix nom dues vegades: si el codi de definició del component s'executa més d'una vegada (per exemple, per un error de configuració del bundler que duplica el mòdul), el navegador llançarà un error indicant que aquest nom ja està definit. La solució habitual és assegurar-se de que cada component es defineix en un únic fitxer que s'importa una sola vegada.
- Esperar que
render()modifiqui el DOM directament:render()no ha de modificar el DOM a mà (per exemple, ambdocument.querySelector); la seva única responsabilitat és retornar la plantilla mitjançanthtml. És Lit qui s'encarrega d'aplicar aquesta plantilla al DOM de manera eficient. - Confondre l'etiqueta HTML amb el nom de la classe: el nom de la classe (
TaskCard) pot ser qualsevol identificador vàlid de JavaScript i no necessita guionets ni relació directa amb el nom de l'etiqueta; és el primer argument decustomElements.define('task-card') el que defineix com s'usarà a l'HTML.
Exercicis
- Crea un component
<user-avatar>que mostri, de manera completament estàtica, el text "AC" dins d'un<span>amb un<div>al voltant (representant, de moment, les inicials d'un usuari fix). Registra'l i usa'l a la teva pàgina de prova. - Afegeix una segona instància de
<task-card>al mateixindex.html, just a sota de la primera. Observa al navegador que apareixen dues targetes idèntiques, cadascuna com una instància independent del component. - Reescriu el component
task-card.jsde l'apartat 5 fent servir el decorador@customElementen lloc decustomElements.define, suposant que el teu projecte té suport de decoradors (la plantilla de Lit de Vite el té).
Solucions
import { LitElement, html } from 'lit';
class UserAvatar extends LitElement {
render() {
return html`
<div>
<span>AC</span>
</div>
`;
}
}
customElements.define('user-avatar', UserAvatar);<script type="module" src="/src/components/user-avatar.js"></script> ... <user-avatar></user-avatar>
En recarregar la pàgina es veuran dues targetes amb exactament el mateix contingut ("Preparar la demo del sprint"), perquè de moment el component és estàtic i no rep cap dada diferent per instància. Aquesta limitació és precisament la que resoldran les propietats reactives al mòdul 3.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('task-card')
class TaskCard extends LitElement {
render() {
return html`
<article>
<h3>Preparar la demo del sprint</h3>
<p>Estado: En curso</p>
<p>Asignada a: Ana</p>
</article>
`;
}
}El comportament al navegador és idèntic al de la versió amb customElements.define; només canvia l'estil d'escriptura.
Conclusió
En aquesta lliçó has creat el teu primer component Lit seguint els tres passos que es repetiran durant tot el curs: heretar de LitElement, implementar render() amb la funció html, i registrar el component amb customElements.define (o el seu equivalent @customElement). Amb aquesta base, has creat la primera versió de <task-card>, encara completament estàtica, sense propietats ni dades dinàmiques: sempre mostra la mateixa tasca d'exemple.
Et deus haver fixat en que, en inspeccionar el component al navegador, apareix un #shadow-root que Lit crea automàticament. A la lliçó següent, "Anatomia d'un Component Lit", s'analitza amb detall què significa això, com s'estructura per dins una classe que estén LitElement, i s'ofereix una primera panoràmica del cicle de vida d'un component, abans de fer el salt, al mòdul 2, a les plantilles reactives que permetran que <task-card> deixi de ser estàtica.
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
