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

  1. Qué es una directiva en Lit
  2. classMap: alternar clases desde un objeto
  3. Reescribiendo la insignia de estado de <task-card> con classMap
  4. Combinar classMap con clases estáticas: lo que sí y lo que no
  5. styleMap: estilos inline dinámicos
  6. ifDefined: omitir un atributo cuando su valor es undefined
  7. Aplicando ifDefined a <user-avatar>
  8. Cuándo NO hace falta una directiva incorporada

  1. 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).

  1. classMap: alternar clases desde un objeto

classMap 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".

  1. Reescribiendo la insignia de estado de <task-card> con classMap

La 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.

  1. Combinar classMap con clases estáticas: lo que sí y lo que no

Una 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:

html`<span class="insignia ${classMap({ 'insignia--hecha': this.estado === 'hecha' })}"></span>`;

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.

  1. styleMap: estilos inline dinámicos

styleMap 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.

  1. ifDefined: omitir un atributo cuando su valor es undefined

El 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:

html`<img alt="${this.descripcion}" />`;

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).

  1. Aplicando 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.

  1. 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 classMap o styleMap con 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 atributo class o style en 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 ifDefined solo reacciona a undefined: como se ha visto en el apartado 6, ni null ni '' ni 0 provocan la eliminación del atributo; si el valor por defecto de una propiedad es '' en lugar de undefined (como ocurría con asignadoImagen en la lección 04-04), ifDefined no tendrá ningún efecto sin cambiar primero ese valor por defecto.
  • Usar styleMap para 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; styleMap no es un sustituto general de static styles ni 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 ramas if seguía siendo perfectamente legítima; classMap aporta 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

  1. Añade a <task-card> una clase tarjeta--compacta que se active mediante una nueva propiedad booleana compacta, combinándola con classMap junto a la clase base tarjeta y a tarjeta--urgente (ya existente por el aviso de urgencia). Escribe el objeto completo que le pasarías a classMap.
  2. Retoma el ejemplo de styleMap del apartado 5 y modifícalo para que borderLeftWidth valga '4px' únicamente cuando this._contadorTiempo.cercaDeVencer sea true (el controlador reactivo de la lección 06-03), en lugar de this.urgente. Explica con tus propias palabras por qué esto sigue siendo una única expresión dinámica válida dentro del atributo style.
  3. Un compañero de equipo escribe <user-avatar imagen-url="${this.asignadoImagen}">, con asignadoImagen por defecto en '' (cadena vacía, como en la lección 04-04), y se sorprende de que ifDefined "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 que ifDefined empezara 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.

  1. ifDefined solo omite el atributo cuando el valor que recibe es exactamente undefined; una cadena vacía '' es un valor perfectamente definido desde el punto de vista de JavaScript, así que ifDefined('') deja pasar la cadena vacía tal cual, y el atributo imagen-url="" se establece con normalidad (vacío, pero presente). Para que ifDefined tenga el efecto deseado, asignadoImagen tendría que valer undefined cuando no hay imagen disponible, en lugar de ''; una forma rápida de conseguirlo sin cambiar el valor por defecto de la propiedad sería escribir ifDefined(this.asignadoImagen || undefined), convirtiendo explícitamente la cadena vacía en undefined justo 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

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados