La última lección del módulo anterior cerraba con una promesa: con TaskFlow funcionalmente completo —<task-board> como orquestador y proveedor de contexto, <task-list> filtrando y renderizando con repeat, <task-card> con sus insignias, su avatar y su urgencia calculada, y <task-filter> leyendo y escribiendo el contexto compartido—, toca ver cómo esa misma aplicación se integra con el resto del mundo: HTML plano, otros frameworks, renderizado en el servidor y empaquetado. Esta lección empieza por el caso más sencillo de todos, y por eso mismo el más revelador: usar un componente Lit dentro de una página HTML corriente, sin ningún proyecto Vite detrás, sin ningún paso de compilación, sin ningún framework de aplicación. Si algo de esto sorprende, es la señal de que conviene repasar por qué es posible antes de ver cómo se hace.
Contenido
- Por qué un componente Lit funciona en cualquier HTML
- Cargar Lit y un componente sin ningún paso de compilación
<script type="module">desde un CDN: unpkg y esm.sh- La limitación de los atributos: por qué no basta con escribir
tareas="..." - Estableciendo propiedades complejas desde JavaScript puro
- Ejemplo completo:
<task-card>en una página HTML estática - Cuándo sigue haciendo falta un proyecto con build
- Por qué un componente Lit funciona en cualquier HTML
La lección 01-01 ya adelantó la idea, sin desarrollarla todavía: un componente Lit, una vez definido con customElements.define(...), es un Custom Element estándar, exactamente el mismo tipo de objeto que <video> o <details>. Lit no inventa un mecanismo propio de registro de componentes ni exige ningún entorno de ejecución especial para que un elemento personalizado funcione; se apoya, en todo momento, en la API customElements que cualquier navegador moderno implementa de forma nativa. La consecuencia práctica es tan directa como sorprendente para quien viene de un framework de aplicación: cualquier página HTML que cargue el código JavaScript de un componente Lit puede usar la etiqueta de ese componente, sin necesidad de React, sin necesidad de Vue, sin necesidad de ningún compilador ni empaquetador.
Esto no es una casualidad de diseño, sino el objetivo explícito de Lit desde su primera lección de este curso: producir componentes que sean, ante todo, estándares de la plataforma web, y que la capa de conveniencia que Lit añade por encima (plantillas reactivas, actualizaciones eficientes, static properties) desaparezca en tiempo de ejecución, dejando solo el Custom Element resultante. Todo el proyecto Vite usado durante el curso hasta ahora ha sido una comodidad para el desarrollo —recarga en caliente, resolución de módulos, servidor local—, no un requisito del propio Lit para funcionar en un navegador.
- Cargar Lit y un componente sin ningún paso de compilación
El proyecto Vite de TaskFlow, usado en todas las lecciones anteriores, resuelve las importaciones (import { LitElement, html, css } from 'lit') a través de node_modules, procesándolas con un empaquetador antes de servirlas al navegador. Una página HTML plana, sin ningún servidor de desarrollo detrás, no tiene ese paso intermedio: el navegador necesita recibir directamente una URL válida para cada módulo que importa. Los navegadores modernos ya saben ejecutar módulos ES nativos con <script type="module">, y ya saben resolver importaciones con ruta relativa o absoluta, pero no saben, por sí mismos, qué archivo corresponde al nombre 'lit' sin más contexto; ese es exactamente el trabajo que hace un empaquetador con node_modules, y que falta por completo en una página sin build.
Hay dos formas de resolver esta ausencia: apuntar directamente a un CDN que sirva Lit ya empaquetado con una URL completa, o generar un bundle propio del componente (incluyendo Lit dentro de él) con las herramientas que se verán en la lección 08-04. Esta lección se centra en la primera opción, la más inmediata para un caso de uso puntual —una demo, una página de documentación, un widget aislado— que no justifica montar un proyecto completo de build.
<script type="module"> desde un CDN: unpkg y esm.sh
<script type="module"> desde un CDN: unpkg y esm.shLos dos CDN más habituales para consumir paquetes npm directamente como módulos ES, sin ningún paso de instalación local, son unpkg y esm.sh. Ambos toman un paquete publicado en npm (como lit) y lo sirven ya convertido a un módulo ES que un navegador puede importar directamente por URL:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>TaskFlow — Demo sin build</title>
</head>
<body>
<task-card titulo="Revisar propuesta" estado="pendiente" prioridad="2"></task-card>
<script type="module">
import { LitElement, html, css } from 'https://esm.sh/lit@3';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
};
render() {
return html`
<article>
<h3>${this.titulo}</h3>
<p>Estado: ${this.estado} · Prioridad: ${this.prioridad}</p>
</article>
`;
}
static styles = css`
article {
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
}
`;
}
customElements.define('task-card', TaskCard);
</script>
</body>
</html>Este fichero, guardado como index.html y abierto directamente en un navegador (o servido por cualquier servidor HTTP mínimo, incluido el propio python -m http.server), renderiza <task-card> sin ningún package.json, sin ninguna instalación de dependencias y sin ningún paso de compilación previo. El navegador descarga https://esm.sh/lit@3 en el momento en que ejecuta el import, exactamente como descargaría cualquier otro recurso externo, y a partir de ahí LitElement, html y css están disponibles para el resto del script, igual que en el proyecto Vite del curso.
La diferencia práctica entre unpkg y esm.sh es sobre todo de formato de URL y de opciones de resolución: esm.sh tiende a generar módulos ES más directamente utilizables sin parámetros adicionales, mientras que unpkg sirve el contenido tal cual está publicado en el paquete npm, que a veces requiere apuntar explícitamente al fichero del build ya en formato de módulo ES (por ejemplo, https://unpkg.com/lit@3/index.js?module). Para un caso de uso puntual, cualquiera de los dos cumple; lo importante es fijar siempre una versión concreta (lit@3, no lit sin más), para que la página no cambie de comportamiento sin aviso si el paquete publica una versión nueva con cambios incompatibles.
- La limitación de los atributos: por qué no basta con escribir
tareas="..."
tareas="..."El ejemplo del apartado anterior usa solo atributos de tipo cadena o número (titulo, estado, prioridad), y por eso funciona sin ningún matiz adicional: HTML es, por definición, un lenguaje de atributos de texto, y Lit convierte automáticamente esas cadenas al tipo declarado en static properties (como se explicó en la lección 03-03 sobre conversores de tipo). El problema aparece en cuanto se necesita establecer una propiedad compleja —un array, un objeto— directamente desde el marcado HTML, algo que TaskFlow necesita constantemente: <task-list> recibe un array completo de tareas a través de su propiedad tareas.
<!-- Esto NO funciona como cabría esperar -->
<task-list tareas="[{"id":"t1","titulo":"Diseñar"}]"></task-list>Nada impide, técnicamente, escribir un atributo con una cadena que contenga JSON serializado a mano, con las comillas escapadas como entidades HTML ("), pero es una solución frágil e incómoda de mantener, y además exige que el propio componente se encargue de invocar JSON.parse(...) sobre el valor del atributo antes de poder usarlo como un array real, un paso que ninguno de los componentes de TaskFlow implementa (tareas se declara { type: Array }, y el conversor por defecto de Lit para Array no hace ese trabajo de deserializar JSON de forma automática y segura para cualquier estructura arbitraria). La limitación de fondo es la misma que ya apareció, desde otro ángulo, en la lección 03-04: los atributos HTML son siempre cadenas de texto; las propiedades de JavaScript pueden ser cualquier valor del lenguaje, incluidos objetos y arrays, pero solo son accesibles desde código, no desde el marcado HTML en sí mismo.
- Estableciendo propiedades complejas desde JavaScript puro
La solución, exactamente igual que dentro del propio Lit, es establecer la propiedad directamente en JavaScript, no como un atributo de marcado:
<task-list id="lista-principal"></task-list>
<script type="module">
const lista = document.querySelector('#lista-principal');
lista.tareas = [
{ id: 't1', titulo: 'Diseñar la base de datos', estado: 'hecha', prioridad: 2 },
{ id: 't2', titulo: 'Implementar autenticación', estado: 'progreso', prioridad: 3 },
{ id: 't3', titulo: 'Escribir pruebas de integración', estado: 'pendiente', prioridad: 1 },
];
</script>document.querySelector('#lista-principal') devuelve la instancia real del elemento <task-list> ya conectado al DOM, y lista.tareas = [...] asigna directamente la propiedad reactiva tareas, exactamente el mismo mecanismo que dispara Lit cuando una plantilla usa .tareas="${...}" con el prefijo de punto (visto por primera vez en la lección 05-03). No hay ninguna diferencia de fondo entre asignar una propiedad desde una plantilla Lit dentro de otro componente y asignarla directamente desde una línea de JavaScript suelta en una página HTML: en ambos casos, Lit detecta el cambio de valor a través del mismo mecanismo de propiedades reactivas estudiado desde el módulo 3, y programa un nuevo renderizado si corresponde.
Este es, en definitiva, el criterio general para decidir entre atributo y propiedad al integrar un componente Lit en HTML plano: cualquier valor que sea de forma natural una cadena, un número o un booleano simple puede escribirse directamente como atributo en el marcado; cualquier valor que sea un objeto, un array, una función o cualquier estructura que no tenga una representación textual razonable necesita asignarse como propiedad desde JavaScript, después de obtener una referencia al elemento con document.querySelector o equivalente.
- Ejemplo completo:
<task-card> en una página HTML estática
<task-card> en una página HTML estáticaUniendo las dos técnicas anteriores, así se vería una página HTML completamente estática, sin ningún proyecto Vite ni paso de build, mostrando una única <task-card> de TaskFlow con datos mixtos —unos como atributos, otro (una función de callback) como propiedad asignada desde JavaScript—:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>TaskFlow — Tarjeta suelta</title>
</head>
<body>
<task-card
id="tarjeta-demo"
titulo="Revisar propuesta de cliente"
estado="progreso"
prioridad="3"
></task-card>
<script type="module">
import 'https://esm.sh/lit@3';
import './task-card.js';
const tarjeta = document.querySelector('#tarjeta-demo');
// asignadoImagen es una URL opcional; se establece como propiedad
// porque, aunque sea una cadena, en este caso concreto conviene
// decidir su valor dinámicamente en JavaScript antes de mostrarla.
tarjeta.asignadoImagen = obtenerImagenDeUsuarioActual();
function obtenerImagenDeUsuarioActual() {
return localStorage.getItem('avatar-url') ?? undefined;
}
</script>
</body>
</html>./task-card.js es aquí un fichero propio del proyecto (no un componente de terceros), que define <task-card> exactamente como se ha construido a lo largo del curso, con la única diferencia de que sus propias importaciones de lit deben resolverse también contra una URL de CDN si el fichero se sirve tal cual, sin ningún paso de build previo (algo que la lección 08-04 explicará cómo evitar, generando en su lugar un bundle autocontenido). El import 'https://esm.sh/lit@3'; suelto, sin capturar ningún valor, no es necesario si task-card.js ya importa Lit por su cuenta desde la misma URL; se incluye aquí solo para dejar explícita la dependencia en el ejemplo.
- Cuándo sigue haciendo falta un proyecto con build
Nada de lo anterior invalida el proyecto Vite usado durante el resto del curso: para un desarrollo serio de TaskFlow, con varios componentes que se importan entre sí, con recarga en caliente durante el desarrollo, con comprobación de tipos (si se opta por TypeScript, como se verá en la lección 08-04) y con un proceso de construcción que optimice el resultado final, un proyecto con build sigue siendo la opción correcta. El caso de esta lección —HTML plano cargando componentes desde un CDN— tiene su sitio en escenarios más acotados: una demo rápida, una página de documentación que muestra un componente aislado, un widget que un tercero debe poder incrustar en su propia web sin depender de la cadena de build de TaskFlow, o simplemente una forma de comprobar que un componente exportado funciona igual de bien fuera del proyecto que lo originó. Reconocer en qué escenario se está, en cada caso, evita tanto montar un proyecto de build innecesario para una demo puntual como intentar mantener una aplicación compleja entera a base de <script type="module"> sueltos sin ninguna herramienta de por medio.
Errores Comunes y Consejos
- Intentar pasar un array o un objeto como atributo de texto: como se explicó en el apartado 4, los atributos HTML son siempre cadenas; una propiedad compleja debe asignarse desde JavaScript (
elemento.propiedad = valor), nunca escribirse serializada a mano dentro de un atributo, salvo que el propio componente implemente explícitamente esa deserialización. - Olvidar fijar una versión concreta al importar desde un CDN: escribir
https://esm.sh/litsin ninguna versión puede servir, en un momento dado, una versión distinta de la que se probó, con cambios de comportamiento inesperados; fijar siemprelit@3(o la versión exacta que corresponda) evita ese riesgo. - Intentar acceder a la propiedad antes de que el elemento esté definido: si el script que asigna
elemento.tareas = [...]se ejecuta antes de quecustomElements.define('task-list', TaskList)haya corrido (por ejemplo, por un orden de scripts incorrecto), la asignación sigue funcionando gracias al mecanismo de upgrade de Custom Elements (Lit recuerda los valores asignados antes de la definición y los aplica en cuanto el elemento se registra), pero conviene no depender de este detalle para código nuevo y mantener siempre el orden lógico: definir el componente antes de manipular sus instancias. - Servir la página con
file://en lugar de un servidor HTTP mínimo: algunos navegadores restringenimportde módulos ES cuando la página se abre directamente desde el sistema de archivos, sin ningún servidor detrás; un servidor HTTP mínimo comonpx serveopython -m http.serverresuelve este problema en segundos y evita errores de CORS confusos.
Ejercicios
- Escribe una página HTML plana, sin ningún proyecto Vite, que cargue Lit desde
https://esm.sh/lit@3, defina un componente<contador-simple>con una propiedad de estadovalor(inicializada a0) y un botón que la incremente en cada clic, y la muestre en su plantilla. - Un compañero de equipo, integrando
<task-list>en una página HTML plana, escribe<task-list tareas="3"></task-list>esperando mostrar tres tareas, y se sorprende de que no funcione. Explica, basándote en el apartado 4, por qué esa expresión no logra el efecto esperado y qué habría que escribir en su lugar. - Retoma el ejemplo del apartado 6 y modifica el script para que, en lugar de asignar
asignadoImagende forma síncrona, la asigne después de una llamada afetch(...)que tarda un tiempo en resolver. ¿Sigue funcionando el mecanismo de propiedades reactivas de Lit igual de bien en ese caso? Razona tu respuesta apoyándote en lo estudiado sobre el ciclo de renderizado en la lección 02-05.
Soluciones
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Contador simple</title>
</head>
<body>
<contador-simple></contador-simple>
<script type="module">
import { LitElement, html } from 'https://esm.sh/lit@3';
class ContadorSimple extends LitElement {
static properties = {
valor: { state: true },
};
constructor() {
super();
this.valor = 0;
}
incrementar() {
this.valor++;
}
render() {
return html`
<p>Valor: ${this.valor}</p>
<button @click="${this.incrementar}">Incrementar</button>
`;
}
}
customElements.define('contador-simple', ContadorSimple);
</script>
</body>
</html>tareas="3"establece un atributo de texto con el valor"3", no una propiedad con un array de tres elementos; como se explicó en el apartado 4, HTML no tiene ninguna sintaxis para expresar directamente "un array con tres tareas" dentro de un atributo, y el conversor de tipoArrayde Lit no interpreta"3"como una instrucción para generar tres tareas de ningún tipo. Para lograr el efecto esperado hace falta obtener una referencia al elemento desde JavaScript y asignar la propiedad directamente, como en el apartado 5:document.querySelector('task-list').tareas = [tarea1, tarea2, tarea3].- Sí, sigue funcionando exactamente igual: el mecanismo de propiedades reactivas de Lit, estudiado desde el módulo 3, no depende de que la asignación ocurra de forma síncrona nada más cargar la página; reacciona a cualquier asignación a la propiedad en cualquier momento de la vida del componente, ya esté conectado desde el principio o se conecte más adelante. En cuanto la promesa de
fetch(...)resuelva y el script ejecutetarjeta.asignadoImagen = ..., Lit detecta el cambio de valor y programa un nuevo renderizado por el mismo ciclo asíncrono por microtarea explicado en la lección 02-05, exactamente como si esa misma asignación hubiera ocurrido dentro de otro componente Lit en lugar de en un script suelto de una página HTML plana.
Conclusión
Esta lección ha mostrado que un componente Lit no necesita ningún entorno especial para funcionar: al ser, en última instancia, un Custom Element estándar, basta con cargar su código —desde un CDN o desde un fichero propio— en cualquier página HTML para usarlo con normalidad, con el único matiz de que las propiedades complejas (arrays, objetos) deben asignarse desde JavaScript, no escribirse como atributos de marcado. Este es el caso más simple de interoperabilidad, pero no el único: la siguiente lección da un paso más y examina cómo se integra un componente Lit dentro de aplicaciones construidas con otros frameworks —React, Vue y Angular—, cada uno con su propio conjunto de fricciones al tratar con Custom Elements que sus propios modelos de componentes no anticipan del todo.
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
