El cierre del módulo anterior dejaba una promesa pendiente: varias veces, a lo largo de este curso, se ha mencionado "de pasada" una herramienta de Lit sin detenerse a explicarla, prometiendo que su momento llegaría en el módulo 7. Ese momento es ahora. Esta lección presenta el concepto de directiva y las tres primeras directivas incorporadas del catálogo de Lit —classMap, styleMap e ifDefined—, aplicándolas directamente sobre código real de TaskFlow escrito en módulos anteriores, para que la mejora se note de inmediato y no quede como una idea abstracta.
Contenido
- Qué es una directiva en Lit
classMap: alternar clases desde un objeto- Reescribiendo la insignia de estado de
<task-card>conclassMap - Combinar
classMapcon clases estáticas: lo que sí y lo que no styleMap: estilos inline dinámicosifDefined: omitir un atributo cuando su valor esundefined- Aplicando
ifDefineda<user-avatar> - Cuándo NO hace falta una directiva incorporada
- Qué es una directiva en Lit
Durante todo este curso, las expresiones ${...} dentro de una plantilla html se han limitado a producir valores corrientes de JavaScript: cadenas, números, booleanos, arrays de plantillas, u otra plantilla html anidada. Lit sabe qué hacer con cada uno de esos tipos porque los reconoce de forma nativa (un array se recorre e inserta cada elemento, una plantilla anidada se inserta como fragmento de DOM, y así sucesivamente, tal como se explicó en el módulo 2).
Una directiva es un tipo especial de valor, reconocible por el motor de plantillas de Lit, que en lugar de convertirse directamente en texto o en un nodo, le indica a Lit un comportamiento concreto para gestionar esa posición de la plantilla. Visualmente, una directiva se usa exactamente igual que cualquier otro valor interpolado —como el resultado de llamar a una función dentro de ${}—, pero internamente el resultado de esa llamada no es una cadena ni un array: es un objeto especial que Lit identifica y trata de forma diferenciada.
import { classMap } from 'lit/directives/class-map.js';
html`<div class="${classMap({ activo: true })}"></div>`;classMap({ activo: true }) no devuelve la cadena "activo"; devuelve un objeto directiva que, colocado en posición de atributo class, le dice a Lit: "gestiona tú el atributo class de este elemento a partir de este objeto, activando o desactivando cada clase según su valor booleano, en cada actualización". Cada directiva incorporada de Lit vive en su propio módulo dentro de lit/directives/, y hay que importarla explícitamente antes de usarla; no forman parte del núcleo de lit que se importa habitualmente (LitElement, html, css).
classMap: alternar clases desde un objeto
classMap: alternar clases desde un objetoclassMap recibe un único argumento: un objeto cuyas claves son nombres de clase CSS y cuyos valores son expresiones booleanas. Por cada clave con valor verdadero, la clase correspondiente se añade al elemento; por cada clave con valor falso, se elimina (o simplemente no se añade, si nunca estuvo presente).
import { classMap } from 'lit/directives/class-map.js';
const clases = {
tarjeta: true,
'tarjeta--urgente': this.urgente,
'tarjeta--expandida': this.expandida,
};
html`<article class="${classMap(clases)}">...</article>`;Frente a la alternativa manual —construir la cadena de clases a mano, algo como `tarjeta ${this.urgente ? 'tarjeta--urgente' : ''} ${this.expandida ? 'tarjeta--expandida' : ''}`.trim()—, classMap elimina por completo la gestión de espacios en blanco, de condicionales anidados y del riesgo de dejar clases "fantasma" activas cuando la condición ya no se cumple. La lógica se reduce a un objeto plano, fácil de leer de un vistazo: cada línea es "esta clase, si esta condición".
- Reescribiendo la insignia de estado de
<task-card> con classMap
<task-card> con classMapLa lección 02-03 introdujo renderInsigniaEstado(), una función auxiliar con tres ramas if que decidía tanto el texto como la clase CSS de la insignia según this.estado:
// Versión de la lección 02-03, sin classMap
renderInsigniaEstado() {
if (this.estado === 'hecha') {
return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
}
if (this.estado === 'progreso') {
return html`<span class="insignia insignia--progreso">⏳ En progreso</span>`;
}
return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
}Esta versión funciona perfectamente bien y no hay ninguna urgencia real por sustituirla —de hecho, cuando el texto visible también cambia según la condición (como aquí, "✓ Hecha" frente a "⏳ En progreso"), unas ramas if explícitas suelen seguir siendo la opción más clara—. Pero sirve como ejemplo perfecto para ver classMap en acción, y el patrón se vuelve claramente superior en cuanto el elemento en sí no cambia, solo su combinación de clases:
// Versión con classMap
renderInsigniaEstado() {
const clases = {
insignia: true,
'insignia--hecha': this.estado === 'hecha',
'insignia--progreso': this.estado === 'progreso',
'insignia--pendiente': this.estado === 'pendiente',
};
const texto = {
hecha: '✓ Hecha',
progreso: '⏳ En progreso',
pendiente: '○ Pendiente',
}[this.estado];
return html`<span class="${classMap(clases)}">${texto}</span>`;
}Aquí classMap sustituye a las tres ramas que decidían la clase, y un objeto literal aparte (texto) sustituye a las que decidían el contenido textual. El resultado tiene una línea más de código que la versión original, así que no es automáticamente "mejor" en este caso concreto; lo interesante es que ambas responsabilidades —qué clase aplicar y qué texto mostrar— quedan separadas y declarativas, en lugar de mezcladas dentro de tres bloques if/return que repiten la misma condición dos veces (una para la clase, otra para el texto). Cuando en el módulo 10 se añadan más estados posibles a TaskFlow, extender esta versión implicará añadir una entrada a cada uno de los dos objetos, sin tocar ninguna rama condicional existente.
- Combinar
classMap con clases estáticas: lo que sí y lo que no
classMap con clases estáticas: lo que sí y lo que noUna duda habitual al empezar a usar classMap es si se puede combinar con clases que no dependen de ninguna condición. La respuesta es sí, siempre que se escriban como texto normal junto a la expresión:
Esto es perfectamente válido: insignia es texto estático del atributo, y classMap(...) aporta el resto de clases condicionales. Lo que no es válido es combinar classMap con una segunda expresión dinámica independiente dentro del mismo atributo class:
// Incorrecto: dos expresiones dinámicas en el mismo atributo class
html`<span class="${this.claseExtra} ${classMap({...})}"></span>`;classMap (igual que styleMap, que se ve en el siguiente apartado) debe ser la única expresión dinámica del atributo, aunque puede convivir con texto literal fijo alrededor. Esta restricción no es arbitraria: classMap necesita compararse a sí misma con la ejecución anterior de la propia directiva para saber qué clases añadir y cuáles quitar, algo que solo puede hacer con garantías si es la única pieza dinámica de la que depende ese atributo.
styleMap: estilos inline dinámicos
styleMap: estilos inline dinámicosstyleMap sigue exactamente el mismo patrón que classMap, pero para el atributo style: recibe un objeto cuyas claves son propiedades CSS (en camelCase, como en JavaScript, o como cadena entre comillas para variables CSS personalizadas) y cuyos valores son las cantidades o cadenas CSS a aplicar.
import { styleMap } from 'lit/directives/style-map.js';
const estilos = {
opacity: this.expandida ? '1' : '0.85',
borderLeftWidth: this.urgente ? '4px' : '2px',
'--color-avatar-tamano': this.compacta ? '32px' : '48px',
};
html`<article style="${styleMap(estilos)}">...</article>`;styleMap resuelve el mismo problema que classMap, trasladado a estilos en línea: en lugar de construir a mano una cadena como `opacity: ${...}; border-left-width: ${...}`, con el riesgo de olvidar un punto y coma o un guion, se declara un objeto plano donde cada propiedad CSS se corresponde con una clave. Al igual que con classMap, styleMap debe ser la única expresión dinámica dentro del atributo style en el que se use, aunque puede combinarse con estilos estáticos escritos como texto alrededor.
Conviene aclarar cuándo styleMap aporta valor frente a lo ya conocido desde el módulo 4: las variables CSS personalizadas siguen siendo la herramienta principal para theming (colores, tamaños configurables desde fuera del componente, como --color-avatar-tamano en el ejemplo). styleMap no sustituye a las variables CSS; las complementa en el caso concreto en que un valor de estilo necesita calcularse dinámicamente desde JavaScript, en cada renderizado, en lugar de configurarse una vez desde fuera del componente.
ifDefined: omitir un atributo cuando su valor es undefined
ifDefined: omitir un atributo cuando su valor es undefinedEl tercer problema que resuelven las directivas de este catálogo es distinto de los dos anteriores. Cuando se interpola una expresión directamente en un atributo normal (no class ni style, sino cualquier otro, como title, alt o un atributo personalizado), Lit convierte el valor a cadena de la forma habitual de JavaScript. El problema aparece cuando ese valor es exactamente undefined:
Si this.descripcion vale undefined (por ejemplo, porque todavía no ha llegado ningún valor desde fuera), el resultado en el DOM no es la ausencia del atributo alt, sino un atributo alt="undefined" literal, con esa palabra visible para cualquier lector de pantalla o herramienta que la inspeccione. Esto rara vez es lo que se quiere: normalmente, si no hay un valor real que ofrecer, lo deseable es que el atributo ni siquiera exista en el DOM.
ifDefined resuelve exactamente este caso:
import { ifDefined } from 'lit/directives/if-defined.js';
html`<img alt="${ifDefined(this.descripcion)}" />`;Con ifDefined, si this.descripcion es undefined, Lit elimina por completo el atributo alt del elemento (o no lo añade, si nunca estuvo presente); si tiene cualquier otro valor —incluida una cadena vacía '', o incluso null—, el atributo se establece con normalidad usando ese valor. Es importante fijarse en ese matiz: ifDefined reacciona únicamente a undefined, no a cualquier valor "falso" en el sentido de JavaScript (0, '' o null siguen estableciendo el atributo con normalidad); si se necesitara omitir el atributo también para una cadena vacía, habría que comprobarlo explícitamente antes de llamar a ifDefined, por ejemplo con ifDefined(this.descripcion || undefined).
- Aplicando
ifDefined a <user-avatar>
ifDefined a <user-avatar><user-avatar>, construido en la lección 04-04, recibe desde <task-card> una propiedad asignadoImagen opcional: cuando la tarea tiene una imagen de la persona asignada, <task-card> distribuye un <img> dentro de <user-avatar>; cuando no, deja el slot vacío para que aparezcan las iniciales de repuesto. Aquella solución usaba una rama if completa en JavaScript, en renderAvatar(), para decidir si construir o no la etiqueta <img> entera:
// Versión de la lección 04-04
renderAvatar() {
if (this.asignadoImagen) {
return html`
<user-avatar nombre="${this.asignadoA}">
<img src="${this.asignadoImagen}" alt="${this.asignadoA}" />
</user-avatar>
`;
}
return html`<user-avatar nombre="${this.asignadoA}"></user-avatar>`;
}Esta solución sigue siendo perfectamente válida, y de hecho es preferible cuando la diferencia entre los dos casos no es solo un atributo, sino la presencia o ausencia de un elemento completo (aquí, la propia etiqueta <img>). Pero conviene conocer la alternativa con ifDefined, útil en un caso ligeramente distinto: cuando lo que se necesita omitir no es un elemento entero, sino un único atributo dentro de un elemento que sí se quiere mantener siempre presente. Supongamos, por ejemplo, que <user-avatar> quisiera aceptar directamente una propiedad imagenUrl opcional y decidir ella misma, internamente, si mostrar una imagen o las iniciales, en lugar de que sea <task-card> quien decida distribuyendo o no un <img>:
// src/components/user-avatar.js (variante con imagenUrl interna)
import { ifDefined } from 'lit/directives/if-defined.js';
render() {
return html`
<div class="avatar" title="${this.nombre}">
<img
src="${ifDefined(this.imagenUrl)}"
alt="${this.nombre}"
class="${classMap({ oculta: !this.imagenUrl })}"
/>
${!this.imagenUrl ? html`<span>${this.iniciales()}</span>` : ''}
</div>
`;
}Si this.imagenUrl es undefined (su valor por defecto, en lugar de una cadena vacía), ifDefined evita que el <img> termine con un src="undefined" literal, que el navegador interpretaría como una petición de red real a una URL inválida, generando un error de carga visible en las herramientas de desarrollador sin ningún motivo real. Nótese que aquí se combinan, en un mismo fragmento, dos de las tres directivas de esta lección: ifDefined para el atributo src, y classMap para ocultar visualmente el <img> vacío mientras se muestran las iniciales de repuesto en su lugar.
- Cuándo NO hace falta una directiva incorporada
Ninguna de las tres directivas de esta lección sustituye por completo a las técnicas ya conocidas del curso; cada una resuelve un problema concreto, y usarlas fuera de ese problema añade una importación y una capa de indirección sin ganar nada a cambio.
| Situación | Técnica recomendada |
|---|---|
| Mostrar o no un elemento completo según una condición | Ternario o && (módulo 2), no classMap |
| Alternar dos o más clases CSS en un mismo elemento que siempre está presente | classMap |
| Un único valor de estilo calculado dinámicamente en JavaScript | styleMap |
Un atributo que a veces no debería existir en absoluto, con un valor undefined |
ifDefined |
Un atributo booleano nativo (disabled, checked, hidden) |
El prefijo ? de Lit (?disabled="${...}"), no ifDefined |
La última fila merece una aclaración: Lit ofrece, desde su sintaxis principal (no como directiva del catálogo de lit/directives/), un prefijo ? para atributos booleanos nativos, que añade o quita el atributo según que el valor sea verdadero o falso, sin necesidad de ifDefined ni de ninguna otra directiva. ifDefined está pensado para atributos con un valor real (una cadena, como alt o src) que a veces no está disponible, no para atributos puramente booleanos.
Errores Comunes y Consejos
- Combinar
classMapostyleMapcon una segunda expresión dinámica en el mismo atributo: como se explicó en el apartado 4, cada una debe ser la única expresión del atributoclassostyleen el que se usa; si se necesita lógica adicional, hay que incorporarla dentro del propio objeto que se le pasa a la directiva, no como una expresión hermana en el mismo atributo. - Olvidar que
ifDefinedsolo reacciona aundefined: como se ha visto en el apartado 6, ninullni''ni0provocan la eliminación del atributo; si el valor por defecto de una propiedad es''en lugar deundefined(como ocurría conasignadoImagenen la lección 04-04),ifDefinedno tendrá ningún efecto sin cambiar primero ese valor por defecto. - Usar
styleMappara valores que deberían ser variables CSS configurables desde fuera: si un valor de estilo no depende de un cálculo hecho en JavaScript en cada renderizado, sino que es simplemente un valor que quien use el componente debería poder personalizar, una variable CSS (módulo 4) sigue siendo la herramienta correcta;styleMapno es un sustituto general destatic stylesni de las variables CSS. - Reescribir código que ya funciona bien solo por usar una directiva: como se ha comentado en el apartado 3,
renderInsigniaEstado()con tres ramasifseguía siendo perfectamente legítima;classMapaporta más valor cuantas más clases condicionales independientes haya que combinar sobre un mismo elemento, no en casos con una única rama de tres alternativas mutuamente excluyentes.
Ejercicios
- Añade a
<task-card>una clasetarjeta--compactaque se active mediante una nueva propiedad booleanacompacta, combinándola conclassMapjunto a la clase basetarjetay atarjeta--urgente(ya existente por el aviso de urgencia). Escribe el objeto completo que le pasarías aclassMap. - Retoma el ejemplo de
styleMapdel apartado 5 y modifícalo para queborderLeftWidthvalga'4px'únicamente cuandothis._contadorTiempo.cercaDeVencerseatrue(el controlador reactivo de la lección 06-03), en lugar dethis.urgente. Explica con tus propias palabras por qué esto sigue siendo una única expresión dinámica válida dentro del atributostyle. - Un compañero de equipo escribe
<user-avatar imagen-url="${this.asignadoImagen}">, conasignadoImagenpor defecto en''(cadena vacía, como en la lección 04-04), y se sorprende de queifDefined"no sirva para nada" al intentar usarlo sobre esa propiedad. Explica, basándote en el apartado 6, por qué ocurre esto y qué cambio haría falta para queifDefinedempezara a tener efecto.
Soluciones
const clases = {
tarjeta: true,
'tarjeta--urgente': this.urgente,
'tarjeta--compacta': this.compacta,
};
html`<article class="${classMap(clases)}">...</article>`;const estilos = {
borderLeftWidth: this._contadorTiempo.cercaDeVencer ? '4px' : '2px',
};
html`<article style="${styleMap(estilos)}">...</article>`;Sigue siendo una única expresión dinámica válida porque, aunque la condición interna cambie de fuente (de this.urgente a this._contadorTiempo.cercaDeVencer), el atributo style completo sigue recibiendo exactamente un único valor: el resultado de la llamada a styleMap(estilos). Lit no distingue de dónde procede la condición usada dentro del objeto; solo le importa que la propia llamada a la directiva sea la única pieza dinámica del atributo.
ifDefinedsolo omite el atributo cuando el valor que recibe es exactamenteundefined; una cadena vacía''es un valor perfectamente definido desde el punto de vista de JavaScript, así queifDefined('')deja pasar la cadena vacía tal cual, y el atributoimagen-url=""se establece con normalidad (vacío, pero presente). Para queifDefinedtenga el efecto deseado,asignadoImagentendría que valerundefinedcuando no hay imagen disponible, en lugar de''; una forma rápida de conseguirlo sin cambiar el valor por defecto de la propiedad sería escribirifDefined(this.asignadoImagen || undefined), convirtiendo explícitamente la cadena vacía enundefinedjusto antes de pasarla a la directiva.
Conclusión
Esta lección ha cerrado tres menciones pendientes desde los módulos 2 y 4: classMap y styleMap como alternativas más cómodas que construir cadenas de clases o estilos a mano, e ifDefined como forma de evitar atributos con el valor literal "undefined" cuando un dato opcional todavía no está disponible. Las tres comparten la misma naturaleza de fondo, presentada en el apartado 1: son directivas, un tipo especial de valor que el motor de plantillas de Lit reconoce y trata de forma diferenciada, más allá de las cadenas, números y plantillas anidadas ya conocidos.
Estas tres directivas incorporadas resuelven problemas puntuales y concretos dentro de una plantilla, pero todas comparten una limitación: no dan acceso directo al nodo del DOM que Lit gestiona en esa posición, ni permiten mantener un estado propio entre renderizados sucesivos más allá del objeto que se les pasa como argumento. La siguiente lección presenta la herramienta que sí ofrece ese nivel de control: las directivas personalizadas, con las que se podrá escribir una lógica de renderizado reutilizable, con acceso real a la parte del DOM afectada, para casos que classMap o styleMap no pueden cubrir por sí solos.
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
