Ha llegado el momento de escribir código de verdad. En esta lección crearás tu primer componente con Lit, empezando por el clásico "Hola Mundo" y avanzando enseguida hacia algo con más sentido dentro de este curso: la primera versión de <task-card>, el componente que representará una tarea individual en TaskFlow. Esta primera versión será deliberadamente simple —sin propiedades ni datos dinámicos— para centrarse en entender la mecánica básica de un componente Lit: cómo se define la clase, cómo se declara su plantilla y cómo se registra como una etiqueta HTML utilizable.
Contenido
- Los tres pasos de todo componente Lit
- Hola Mundo con LitElement
- Registrar el componente:
customElements.definey@customElement - Usar el componente como una etiqueta HTML normal
- Primera versión de
<task-card> - Verlo funcionar en la página
- Los tres pasos de todo componente Lit
Todo componente hecho con Lit, por complejo que llegue a ser más adelante en el curso, se construye siempre siguiendo los mismos tres pasos:
- Extender
LitElement: se crea una clase de JavaScript que hereda deLitElement, la clase base que proporciona Lit. - Implementar
render(): dentro de esa clase se define un métodorender()que devuelve la plantilla HTML del componente, usando la funciónhtml. - Registrar el elemento: se le da un nombre de etiqueta al componente mediante
customElements.define(o el decorador equivalente@customElement), para que el navegador sepa qué clase debe usar cuando encuentre esa etiqueta en el HTML.
El resto de esta lección desarrolla cada uno de estos tres pasos con ejemplos concretos.
- Hola Mundo con LitElement
Empecemos por el ejemplo más simple posible. Dentro del proyecto taskflow creado en la lección anterior, crea un fichero src/components/hola-mundo.js con el siguiente contenido:
import { LitElement, html } from 'lit';
class HolaMundo extends LitElement {
render() {
return html`<p>¡Hola Mundo desde Lit!</p>`;
}
}
customElements.define('hola-mundo', HolaMundo);Analicemos cada línea:
import { LitElement, html } from 'lit';: se importan las dos piezas que se van a usar.LitElementes la clase base de la que hereda cualquier componente Lit;htmles una función especial (una "función de plantilla etiquetada", una característica estándar de JavaScript) que permite escribir HTML de forma legible dentro del código JavaScript.class HolaMundo extends LitElement { ... }: se declara una clase normal de JavaScript que hereda deLitElement. A partir de aquí,HolaMundotiene ya todas las capacidades que ofrece Lit (renderizado reactivo, ciclo de vida, etc.), aunque en este primer ejemplo no se use ninguna de las más avanzadas.render() { return html\¡Hola Mundo desde Lit!
`; }: el métodorender()es el corazón de cualquier componente Lit. Lit lo invoca automáticamente cuando el componente necesita mostrarse (o actualizarse) y espera que devuelva el resultado de la funciónhtml`. Aquí, simplemente se devuelve un párrafo con un texto fijo.customElements.define('hola-mundo', HolaMundo);: esta línea registra la claseHolaMundocomo la implementación de la etiqueta<hola-mundo>. A partir de este momento, cualquier<hola-mundo>que aparezca en el HTML de la página será "activado" por el navegador usando esta clase.
Un detalle importante sobre la función html: aunque a simple vista parezca una simple cadena de texto, no lo es. html es una plantilla etiquetada que Lit analiza de forma especial, lo que le permite, entre otras cosas, saber exactamente qué partes de la plantilla son fijas y cuáles son dinámicas (esto se explota a fondo en el módulo de plantillas reactivas). Por ahora, con una plantilla completamente estática como esta, basta con saber que el contenido entre los acentos graves (`) es el HTML que se va a mostrar.
- Registrar el componente:
customElements.define y @customElement
customElements.define y @customElementLa llamada a customElements.define('hola-mundo', HolaMundo) no es una particularidad de Lit: es la forma estándar en la que la especificación de Custom Elements (vista en la lección anterior) registra cualquier elemento personalizado, con o sin Lit de por medio. Lit no sustituye este mecanismo, simplemente lo usa.
Existe una forma alternativa de escribir este registro, mediante un decorador, si el proyecto tiene soporte para decoradores (como es el caso de la plantilla de Lit de Vite vista en la lección 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>`;
}
}Ambas formas son equivalentes: @customElement('hola-mundo') es simplemente una manera más concisa de escribir, justo encima de la clase, lo mismo que customElements.define('hola-mundo', HolaMundo) hace después de ella. La tabla siguiente resume cuándo conviene cada estilo:
| Estilo | Ventaja | Cuándo usarlo |
|---|---|---|
customElements.define(...) |
No requiere ninguna configuración especial de compilación; funciona en JavaScript puro sin pasos adicionales | Proyectos en JavaScript plano, sin Babel/TypeScript configurado para decoradores |
@customElement(...) |
Más conciso y legible; mantiene el nombre de la etiqueta pegado a la definición de la clase | Proyectos en TypeScript, o en JavaScript con Babel configurado para soportar decoradores (como la plantilla de Vite) |
Un requisito importante, común a ambos estilos y heredado directamente del estándar de Custom Elements: el nombre de la etiqueta debe contener siempre al menos un guion (hola-mundo, task-card, user-avatar...). Esta regla existe para garantizar que las etiquetas personalizadas nunca choquen con las etiquetas nativas del HTML actuales o futuras, que nunca llevan guion.
Este curso usará mayoritariamente customElements.define, por ser la forma que funciona sin ninguna configuración adicional, mostrando @customElement como alternativa cuando aporte claridad.
- Usar el componente como una etiqueta HTML normal
Una vez registrado, <hola-mundo> se usa exactamente igual que cualquier etiqueta nativa de HTML. No hace falta "montarlo" mediante una función especial ni pasarle nada a través de JavaScript para que aparezca: basta con escribirlo en el 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 detalles a tener en cuenta:
- El
<script>que carga el componente debe tenertype="module", porque el fichero usaimport, una característica de los módulos ES estándar de JavaScript. - El elemento
<hola-mundo></hola-mundo>se puede colocar en cualquier parte del<body>, tantas veces como se quiera, igual que se haría con un<div>o un<button>. Cada aparición de la etiqueta crea una instancia independiente del componente.
- Primera versión de
<task-card>
<task-card>Con la mecánica básica ya clara, es el momento de crear el primer componente real de TaskFlow: <task-card>. En esta lección será una versión deliberadamente simple y totalmente estática: mostrará siempre el mismo contenido de ejemplo, sin ninguna propiedad configurable ni ningún dato dinámico. La capacidad de personalizar cada tarjeta con datos reales (título, estado, persona asignada...) llegará con las propiedades reactivas, en el módulo 3.
Crea el fichero 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);Este componente sigue exactamente los mismos tres pasos vistos en el apartado 1: hereda de LitElement, implementa render() devolviendo una plantilla html (en este caso con varias etiquetas anidadas: un <article> que envuelve un título y dos párrafos) y se registra con customElements.define bajo el nombre task-card.
Para usarlo, se puede crear una pequeña página de prueba, index.html, en la raíz del proyecto (si usas la estructura de Vite, sustituye el contenido de ejemplo que trae por defecto):
<!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>
- Verlo funcionar en la página
Con el servidor de desarrollo arrancado (npm run dev, como se explicó en la lección anterior) y el navegador abierto en la URL local, deberías ver la tarjeta renderizada en la página. Un ejercicio útil en este punto es abrir las herramientas de desarrollo del navegador (F12) e inspeccionar el elemento <task-card>: si Shadow DOM estuviera activado (todavía no lo hemos usado explícitamente, ya que no se ha llamado a attachShadow ni se ha usado la plantilla dentro de un render con Shadow DOM propio de Lit), verías el contenido del <article> como hijos directos del elemento en el DOM normal ("light DOM").
Es importante aclarar un matiz aquí: aunque en este ejemplo no se ha mencionado explícitamente, LitElement sí usa Shadow DOM por defecto para renderizar el contenido devuelto por render(). Es decir, al inspeccionar <task-card> en las herramientas de desarrollo, en realidad verás una etiqueta especial #shadow-root dentro del elemento, y el <article> colgando de ella, no directamente del <task-card>. Esto es exactamente lo esperado: es el comportamiento por defecto de cualquier componente Lit. El siguiente tema de este mismo módulo explica con más detalle qué implica esto y por qué es así; de momento, si ves ese #shadow-root en el inspector, es la confirmación de que todo funciona correctamente.
Errores Comunes y Consejos
- Olvidar
type="module"en el<script>: si el componente no aparece y la consola del navegador muestra un error relacionado conimport, lo primero que hay que comprobar es que el<script>que carga el fichero tiene el atributotype="module". - Nombre de etiqueta sin guion:
customElements.define('taskcard', TaskCard)lanzará un error en tiempo de ejecución, porque el estándar exige al menos un guion en el nombre. La forma correcta estask-card. - Registrar el mismo nombre dos veces: si el código de definición del componente se ejecuta más de una vez (por ejemplo, por un error de configuración del bundler que duplica el módulo), el navegador lanzará un error indicando que ese nombre ya está definido. La solución habitual es asegurarse de que cada componente se define en un único fichero que se importa una sola vez.
- Esperar que
render()modifique el DOM directamente:render()no debe modificar el DOM a mano (por ejemplo, condocument.querySelector); su única responsabilidad es devolver la plantilla mediantehtml. Es Lit quien se encarga de aplicar esa plantilla al DOM de forma eficiente. - Confundir la etiqueta HTML con el nombre de la clase: el nombre de la clase (
TaskCard) puede ser cualquier identificador válido de JavaScript y no necesita guiones ni relación directa con el nombre de la etiqueta; es el primer argumento decustomElements.define('task-card') el que define cómo se usará en el HTML.
Ejercicios
- Crea un componente
<user-avatar>que muestre, de forma completamente estática, el texto "AC" dentro de un<span>con un<div>alrededor (representando, de momento, las iniciales de un usuario fijo). Regístralo y úsalo en tu página de prueba. - Añade una segunda instancia de
<task-card>en el mismoindex.html, justo debajo de la primera. Observa en el navegador que aparecen dos tarjetas idénticas, cada una como una instancia independiente del componente. - Reescribe el componente
task-card.jsdel apartado 5 usando el decorador@customElementen lugar decustomElements.define, suponiendo que tu proyecto tiene soporte de decoradores (la plantilla de Lit de Vite lo tiene).
Soluciones
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>
Al recargar la página se verán dos tarjetas con exactamente el mismo contenido ("Preparar la demo del sprint"), porque de momento el componente es estático y no recibe ningún dato distinto por instancia. Esta limitación es precisamente la que resolverán las propiedades reactivas en el módulo 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 comportamiento en el navegador es idéntico al de la versión con customElements.define; solo cambia el estilo de escritura.
Conclusión
En esta lección has creado tu primer componente Lit siguiendo los tres pasos que se repetirán durante todo el curso: heredar de LitElement, implementar render() con la función html, y registrar el componente con customElements.define (o su equivalente @customElement). Con esa base, has creado la primera versión de <task-card>, todavía completamente estática, sin propiedades ni datos dinámicos: siempre muestra la misma tarea de ejemplo.
Te habrás fijado en que, al inspeccionar el componente en el navegador, aparece un #shadow-root que Lit crea automáticamente. En la siguiente lección, "Anatomía de un Componente Lit", se analiza con detalle qué significa esto, cómo se estructura por dentro una clase que extiende LitElement, y se ofrece una primera panorámica del ciclo de vida de un componente, antes de dar el salto, en el módulo 2, a las plantillas reactivas que permitirán que <task-card> deje de ser estática.
Curso de Lit
Módulo 1: Introducción a Lit y Web Components
- ¿Qué son los Web Components y por qué Lit?
- Configuración del Entorno de Desarrollo
- Tu Primer Componente Lit
- Anatomía de un Componente Lit
Módulo 2: Plantillas Reactivas y Renderizado
- El Motor de Plantillas de Lit
- Expresiones e Interpolación en Plantillas
- Renderizado Condicional
- Renderizado de Listas
- El Ciclo de Renderizado
Módulo 3: Propiedades y Estado Reactivo
- Propiedades Reactivas
- Estado Interno con @state
- Tipos de Propiedades y Conversores Personalizados
- Atributos vs Propiedades y Reflexión
Módulo 4: Estilos en Componentes Lit
- CSS Encapsulado con Shadow DOM
- Estilos Compartidos entre Componentes
- Variables CSS Personalizadas y Theming
- Slots y Estilizado de Contenido Distribuido
Módulo 5: Eventos y Comunicación entre Componentes
- Manejo de Eventos DOM en Plantillas
- Eventos Personalizados: Comunicación Hijo a Padre
- Comunicación de Padre a Hijo con Propiedades
- Patrones de Comunicación entre Componentes Hermanos
Módulo 6: Ciclo de Vida y Comportamiento Avanzado
- Callbacks del Ciclo de Vida
- Hooks Reactivos: willUpdate, updated y firstUpdated
- Controladores Reactivos
- Mixins y Composición de Comportamiento
Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas
- Directivas Incorporadas: classMap, styleMap e ifDefined
- Directivas Personalizadas
- Renderizado Asíncrono con until
- Contexto Compartido con @lit/context
Módulo 8: Integración, Interoperabilidad y Despliegue
- Usar Componentes Lit en HTML Plano
- Integrar Lit con React, Vue y Angular
- Renderizado en el Servidor con @lit-labs/ssr
- Empaquetado, Publicación y TypeScript
Módulo 9: Pruebas y Buenas Prácticas
- Pruebas Unitarias con Web Test Runner
- Accesibilidad en Web Components
- Rendimiento y Optimización
- Patrones y Anti-patrones Comunes
