En la lección anterior creaste tu primer componente Lit sin detenerte demasiado en el porqué de cada pieza. Antes de avanzar hacia las plantillas reactivas, conviene hacer una pausa y entender con detalle la anatomía completa de una clase que extiende LitElement: qué partes la componen, cómo se relaciona con el estándar de Custom Elements que ya conoces, qué implica que use Shadow DOM por defecto, y qué momentos de vida atraviesa un componente desde que nace hasta que desaparece. Esta lección es la última teórica del módulo y cierra el círculo abierto en la primera lección, dejando el terreno preparado para el módulo 2, donde empezarás a trabajar con plantillas realmente dinámicas.
Contenido
- Estructura general de una clase LitElement
- Declarar propiedades: decoradores frente a
static properties - LitElement es HTMLElement: la relación con el estándar
- Shadow DOM por defecto: qué significa y qué implica
- Panorama del ciclo de vida de un componente Lit
- Hacia el módulo 2: plantillas reactivas
- Estructura general de una clase LitElement
Toda clase que extiende LitElement puede contener, en general, los siguientes bloques (ninguno es obligatorio salvo render(), que es el único imprescindible para que el componente muestre algo):
import { LitElement, html, css } from 'lit';
class TaskCard extends LitElement {
// 1. Declaración de propiedades reactivas (se detalla en el módulo 3)
static properties = {
titulo: { type: String },
};
// 2. Estilos encapsulados (se detallan en el módulo 4)
static styles = css`
:host {
display: block;
border: 1px solid #ccc;
}
`;
// 3. Constructor: inicialización del estado interno
constructor() {
super();
this.titulo = 'Tarea sin título';
}
// 4. Callbacks del ciclo de vida (se detallan en el módulo 6)
connectedCallback() {
super.connectedCallback();
}
// 5. Plantilla del componente (obligatorio)
render() {
return html`<h3>${this.titulo}</h3>`;
}
}
customElements.define('task-card', TaskCard);No te preocupes si algunas de estas piezas (static properties, static styles, connectedCallback) todavía no tienen mucho sentido: se explicarán en profundidad en los módulos 3, 4 y 6 respectivamente. El objetivo de este repaso es que, al llegar a esos módulos, ya tengas clara la "forma" general de la clase y sepas ubicar cada pieza nueva dentro de un esquema conocido. De hecho, fíjate en que el componente task-card.js de la lección anterior era, simplemente, una versión de esta misma estructura en la que solo se rellenaba el bloque 5 (render()); todos los demás bloques son opcionales y se añaden según las necesidades del componente.
Un detalle sobre el constructor: si se define, es obligatorio llamar primero a super(), como en cualquier clase de JavaScript que hereda de otra. Es el lugar habitual para inicializar valores por defecto de las propiedades del componente, algo que se retomará con más detalle en el módulo 3.
- Declarar propiedades: decoradores frente a
static properties
static propertiesEn la lección anterior ya viste que existen dos estilos para registrar un componente: customElements.define y el decorador @customElement. Esa misma dualidad de estilos se repite para declarar las propiedades reactivas de un componente (su estudio completo corresponde al módulo 3, pero conviene reconocer ya la sintaxis, porque aparecerá en ejemplos de este curso y en la documentación oficial de Lit).
Con decoradores (requiere soporte de compilación, como se explicó en la lección de configuración del entorno):
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>`;
}
}Con static properties, en JavaScript plano sin ningún paso de compilación adicional:
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);| Aspecto | Con decoradores (@property) |
Con static properties |
|---|---|---|
| Requiere compilación adicional | Sí (TypeScript o Babel con plugin de decoradores) | No, JavaScript estándar |
| Dónde se declara el valor por defecto | Junto a la propia declaración de la propiedad | En el constructor, tras super() |
| Verbosidad | Más compacta | Ligeramente más extensa |
| Resultado en tiempo de ejecución | Idéntico | Idéntico |
Ambos estilos producen exactamente el mismo comportamiento; la elección es una cuestión de preferencia y de las herramientas disponibles en el proyecto. Este curso, como ya se explicó en la lección de configuración del entorno, usará principalmente static properties, mostrando la variante con decoradores como referencia cuando resulte útil para quien trabaje con TypeScript en sus propios proyectos.
- LitElement es HTMLElement: la relación con el estándar
Es fundamental interiorizar una idea que ya se apuntó en la primera lección del módulo: LitElement no sustituye el estándar de Custom Elements, lo extiende. En términos de JavaScript, la cadena de herencia es:
HTMLElementes la clase base de la que heredan todos los elementos del DOM, tanto los nativos del navegador (HTMLButtonElement,HTMLDivElement...) como cualquier Custom Element.ReactiveElementes una clase intermedia que Lit añade para incorporar el sistema de propiedades reactivas y el ciclo de actualización, sin todavía imponer un motor concreto de plantillas.LitElementhereda deReactiveElementy añade el métodorender()junto con el uso de la funciónhtmlcomo motor de plantillas.- Tu clase (
TaskCard, en el ejemplo) hereda deLitElementy hereda, por tanto, todas las capacidades de las tres clases anteriores.
La consecuencia práctica de esta cadena de herencia es que un componente Lit es, literalmente, un HTMLElement. Esto significa que hereda toda la API estándar de un elemento del DOM: se le pueden añadir y quitar atributos (setAttribute, getAttribute), se le pueden escuchar y despachar eventos (addEventListener, dispatchEvent, algo que se explota a fondo en el módulo de eventos), tiene acceso a classList, y participa en el árbol del DOM exactamente igual que cualquier otro elemento. Nada de esto es una simulación ni una capa aparte: es la propia plataforma web.
- Shadow DOM por defecto: qué significa y qué implica
En la lección anterior, al inspeccionar <task-card> en las herramientas de desarrollo, apareció un nodo #shadow-root que quizá resultó sorprendente. Esto ocurre porque LitElement, por defecto, llama internamente a attachShadow({ mode: 'open' }) sobre cada instancia del componente y renderiza el resultado de render() dentro de ese Shadow DOM, no directamente como hijos del elemento en el DOM normal.
// Esto es, de forma simplificada, lo que hace LitElement por ti
// la primera vez que el componente necesita renderizarse:
const raizSombra = this.attachShadow({ mode: 'open' });
raizSombra.innerHTML = /* resultado de render() */;¿Qué implica esto en la práctica, sin entrar todavía en el detalle de los estilos (que corresponde al módulo 4)?
- Encapsulación del HTML interno: el contenido devuelto por
render()no es visible mediantedocument.querySelectordesde fuera del componente; hay que acceder primero aelemento.shadowRooty buscar dentro de él. - Encapsulación de los estilos: cualquier CSS que se declare dentro del componente (con
static styles, como se vio en el apartado 1) no se filtra hacia el resto de la página, y los estilos globales de la página tampoco entran por accidente en el componente. Esto evita uno de los problemas más habituales al construir interfaces grandes: colisiones de nombres de clases CSS entre componentes distintos. - Contenido distribuido con
<slot>: el Shadow DOM también define un mecanismo,<slot>, para permitir que un componente reciba contenido HTML "de fuera" y lo posicione dentro de su propia estructura interna. Este mecanismo se explorará en detalle en el módulo de estilos.
Por ahora, el punto clave que hay que retener es este: cada componente Lit vive, por defecto, en su propia burbuja aislada del resto de la página, tanto en estructura como en estilos. Esta es una de las razones por las que Lit resulta especialmente adecuado para construir bibliotecas de componentes reutilizables como los de TaskFlow: se pueden combinar <task-card>, <task-list> y <user-avatar> sin miedo a que los estilos de uno interfieran con los de otro.
- Panorama del ciclo de vida de un componente Lit
Un componente Lit, igual que cualquier Custom Element, no existe de forma estática: nace, se actualiza posiblemente muchas veces, y en algún momento puede desaparecer del DOM. Lit ofrece una serie de métodos ("callbacks del ciclo de vida") que se ejecutan automáticamente en distintos momentos de esa vida, y que un componente puede sobrescribir para ejecutar lógica propia en el momento adecuado.
En esta lección solo se presenta un mapa general, con el nombre de cada callback y para qué sirve a grandes rasgos; el funcionamiento detallado, con ejemplos de uso real, se estudia en profundidad en el módulo 6 ("Ciclo de Vida y Comportamiento Avanzado").
| Momento del ciclo de vida | Callback | Para qué sirve, a grandes rasgos |
|---|---|---|
| El elemento se inserta en el DOM | connectedCallback() |
Heredado directamente del estándar de Custom Elements; buen lugar para suscribirse a eventos externos o iniciar temporizadores |
| El elemento se retira del DOM | disconnectedCallback() |
También del estándar; lugar habitual para "limpiar" lo que se inició en connectedCallback (cancelar suscripciones, temporizadores...) |
| Antes de aplicar una actualización | willUpdate(changedProperties) |
Propio de Lit; permite calcular valores derivados justo antes de renderizar, en función de qué propiedades han cambiado |
| Se genera la nueva plantilla | render() |
El único obligatorio; devuelve el HTML actual del componente |
| Después de aplicar una actualización al DOM | updated(changedProperties) |
Propio de Lit; útil para reaccionar a cambios ya reflejados en el DOM real |
| Justo después de la primera actualización | firstUpdated(changedProperties) |
Propio de Lit; se ejecuta una única vez, ideal para inicializaciones que necesitan que el DOM interno ya exista |
Dos ideas importantes para quedarse, aunque el detalle llegue más adelante:
- Los callbacks
connectedCallbackydisconnectedCallbackno son una invención de Lit: pertenecen al estándar de Custom Elements visto en la primera lección del módulo. Lit los aprovecha y añade su propia capa de callbacks adicionales (willUpdate,updated,firstUpdated) específicos de su sistema de renderizado reactivo. - Ningún componente necesita implementar todos estos callbacks. La mayoría de los componentes sencillos, como el
<task-card>estático de la lección anterior, no necesitan ninguno de ellos; se van incorporando a medida que el componente necesita reaccionar a momentos concretos de su vida.
- Hacia el módulo 2: plantillas reactivas
Con esta lección se cierra el módulo de introducción. Ya conoces la base sobre la que se apoya todo lo demás: qué son los Web Components como estándar, qué aporta Lit sobre ellos, cómo configurar un entorno de trabajo, cómo se ve por dentro una clase LitElement, y qué momentos de vida atraviesa un componente.
Sin embargo, todos los ejemplos vistos hasta ahora comparten una limitación deliberada: sus plantillas son completamente estáticas. El <task-card> que has construido siempre muestra la misma tarea de ejemplo, sin importar qué ocurra en la aplicación. Esa limitación es exactamente el punto de partida del módulo 2, "Plantillas Reactivas y Renderizado", donde aprenderás a insertar expresiones dinámicas dentro de la función html, a mostrar u ocultar contenido según condiciones, a renderizar listas completas de tareas, y a entender cómo y cuándo decide Lit volver a ejecutar render().
Errores Comunes y Consejos
- Intentar acceder al contenido interno del componente con
document.querySelectordesde fuera: como el contenido vive dentro de un Shadow DOM,document.querySelector('task-card h3')no encontrará nada. Hay que acceder primero al propio elemento y luego a sushadowRoot(elemento.shadowRoot.querySelector('h3')), o mejor aún, evitar manipular el DOM interno manualmente y dejar que searender()quien decida qué se muestra. - Olvidar llamar a
super()en el constructor: si se define unconstructorpersonalizado sin invocar primero asuper(), el navegador lanzará un error en tiempo de ejecución. Esta regla no es específica de Lit, sino de cualquier clase de JavaScript que herede de otra. - Pensar que hay que implementar todos los callbacks del ciclo de vida: como se ha visto en la tabla del apartado 5, la mayoría de los componentes solo necesitan
render(). Añadir callbacks "por si acaso" complica el componente sin necesidad. - Confundir
ReactiveElementconLitElement: en el día a día casi nunca se interactúa directamente conReactiveElement; se menciona aquí solo para entender la cadena de herencia. En la práctica, todos los componentes de este curso extenderánLitElementdirectamente.
Ejercicios
- Dibuja (en un papel o en un fichero de texto) la cadena de herencia completa de un componente
UserAvatarque definas tú mismo, desdeHTMLElementhasta tu clase, indicando qué aporta cada eslabón de la cadena. - Retoma el componente
task-card.jsde la lección anterior (la versión estática) y añade un métodoconnectedCallback()que, medianteconsole.log, escriba en la consola del navegador el mensaje "task-card conectado al DOM". Recuerda llamar asuper.connectedCallback(). - Explica con tus propias palabras, en dos o tres frases, por qué el hecho de que
LitElementuse Shadow DOM por defecto es una ventaja para un proyecto como TaskFlow, que tendrá varios componentes distintos conviviendo en la misma página.
Soluciones
- La cadena, igual que la de
TaskCardvista en el apartado 3, es:
HTMLElement (estándar del navegador: API base de cualquier elemento del DOM)
→ ReactiveElement (Lit: sistema de propiedades reactivas y ciclo de actualización)
→ LitElement (Lit: motor de plantillas mediante render() y html)
→ UserAvatar (tu clase: la lógica y plantilla concretas del 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);Al recargar la página con las herramientas de desarrollo abiertas, debería aparecer el mensaje en la consola por cada <task-card> presente en el HTML.
- Una posible respuesta: al estar cada componente encapsulado en su propio Shadow DOM, los estilos CSS de
<task-card>,<task-list>o<user-avatar>no pueden interferir entre sí ni con los estilos globales de la página, aunque usen los mismos nombres de clase o de etiqueta. Esto permite desarrollar y mantener cada componente de forma independiente, sin necesidad de coordinar convenciones de nombres CSS entre todo el equipo, algo especialmente valioso a medida que TaskFlow vaya sumando más componentes a lo largo del curso.
Conclusión
En esta lección has terminado de desmontar la anatomía de un componente Lit: su estructura general con bloques opcionales y un único método obligatorio (render()), las dos formas de declarar propiedades (decoradores y static properties), su relación directa con HTMLElement a través de la cadena de herencia HTMLElement → ReactiveElement → LitElement, el uso del Shadow DOM por defecto para encapsular estructura y estilos, y una primera panorámica —solo los nombres y su propósito general— del ciclo de vida que se detallará en el módulo 6.
Con esto se cierra el módulo de introducción. En el módulo 2, "Plantillas Reactivas y Renderizado", dejarás atrás los componentes estáticos: aprenderás a insertar expresiones dinámicas en la función html, a renderizar condicionalmente contenido, a mostrar listas de elementos y a entender en profundidad el ciclo de renderizado de Lit, dando a <task-card>, <task-list> y el resto de piezas de TaskFlow su primera capa real de vida dinámica.
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
