Ya habremos visto en temas como plantillas y DOM en componentes que crear marcado HTML en nuestros WebComponents no es algo imposible, pero muchas veces es un trabajo a bajo nivel que puede volverse cuesta arriba.

Plantillas con lit-html

Lit-HTML es una pequeña y ligera librería (sin dependencias) que permite crear plantillas HTML de una forma práctica, cómoda y eficiente, ideal para su uso con WebComponents. Aunque en este artículo la utilizamos de forma independiente, hay que saber que forma parte del proyecto Lit (antiguamente, LitElement), del que hablaremos más adelante, por lo que también podremos utilizarla importando html desde Lit. Sin embargo, lit-html puede utilizarse de forma agnóstica, ya que es totalmente independiente.

Preparación de lit-html

Como viene siendo habitual en librerías, el primer paso es cargar la librería en nuestro código. Para ello, utilizaremos import desde nuestro fichero index.js (o el archivo principal de Javascript que estemos usando). Recuerda asegurarte de tener añadido el atributo type="module" en la etiqueta <script>, ya que de lo contrario el navegador no podrá importar librerías externas.

Tenemos 2 métodos para cargar la librería lit-html, desde un CDN o desde npm, este último previo npm install lit-html:

<!-- Método 1: JS desde CDN/URL -->
<script type="module">
  import { html, render } from "https://unpkg.com/lit-html";
</script>

<!-- Método 2: JS desde NPM -->
<script type="module">
  import { html, render } from "lit-html";
</script>

Con uno de estos dos métodos deberíamos tener disponible la librería en nuestro código, lista para ser utilizada. Observa que en el primer ejemplo estamos utilizando la librería desde un CDN llamado unpkg, que devuelve el script del paquete de NPM indicado en la versión escogida. En el segundo ejemplo, estamos escogiendo el paquete lit-html de los paquetes instalados con npm.

En cualquiera de los dos métodos, lo que hacemos es importar dos funciones que son la base de la librería y comentaremos a continuación: html y render.

En este artículo se asume que usas un servidor local de desarrollo. Si no tienes uno aún, echa un vistazo a nuestro artículo de vite, donde lo instalamos y configuramos.

Creando plantillas básicas

Para crear plantillas con lit-html tenemos que separar y entender bien dos conceptos diferentes:

  • Crear una plantilla: Escribir y preparar marcado HTML reutilizable.
  • Renderizar una plantilla: Mezclarlo con datos y hacerlo efectivo para mostrarlo en la página.

El primer concepto es de lo que se encarga la función html que hemos importado, y el segundo de lo que se encarga la función render. Un pequeño ejemplo básico para ilustrarlo:

<div id="app"></div>

<script type="module">
  import { html, render } from "https://unpkg.com/lit-html";

  const app = document.querySelector("#app");
  const template = (name) => html`<h1>Hello, my friend <strong>${name}</strong></h1>`;

  // Render
  render(template("Manz"), app);

  // Re-render
  render(template("Justin"), app);
</script>

Obsérvese que lo que estamos haciendo es crear una función template(name) que devolverá un marcado HTML que depende de la variable name que le pasemos a la función template.

En la línea siguiente, utilizamos la función render, que analiza la plantilla creada con html, le aplica los datos suministrados por parámetro y los inserta en el elemento del DOM del segundo parámetro. Es decir, renderiza la información.

La potencia real de la función render es que si la ejecutamos nuevamente, render no sobreescribe el contenido existente, sino que es capaz de detectar las partes que cambian, actualizando sólo dichas partes, y por tanto, haciendo esta operación de forma muy rápida.

Eventos con lit-html

Al igual que estamos creando marcado HTML, nos podría interesar insertar eventos, algo que siempre fue complicado hacer en Javascript nativo utilizando string templates. Sin embargo, lit-html permite gestionar eventos de forma muy sencilla, muy similar a como lo hacen frameworks como Vue:

<div id="app"></div>

<script type="module">
  import { html, render } from "https://unpkg.com/lit-html";

  const app = document.querySelector("#app");
  const button = (text, action) => html`<button @click=${action}>${text}</button>`;
  const action = () => alert("Clicked!!!");

  // Render
  render(button("Click me", action), app);
</script>

En esta ocasión, creamos una función button() que generará una plantilla para un botón. El primer parámetro es el texto del botón y el segundo la función que se dispara al hacer click en el botón.

Todo esto se gestiona con un evento. En lugar de escribir onclick (como se suele hacer en Javascript), escribimos @click, que es como gestionaríamos los eventos con lit-html. Luego, en el valor, a través de ${} hacemos referencia a la función que queremos ejecutar.

Condicionales en lit-html

Por otro lado, si quisieramos hacer condicionales en lit-html, sería tan fácil como hacer simples if o usar el operador ternario, como se muestra a continuación:

<div id="app"></div>

<script type="module">
  import { html, render } from "https://unpkg.com/lit-html";

  const app = document.querySelector("#app");
  const isOpen = true;

  const template = (bool) => html`<p>Status: ${ bool ? "Open" : "Closed" }</p>`;
  render(template(isOpen), app);
</script>

Ten en cuenta que en el caso de "Closed" no se quisiera mostrar nada, una cadena vacía seguiría creando un nodo vacío. Para ello podemos importar junto a html y render, el valor nothing, el cuál no creará ningún nodo (ni siquiera vacío).

Muestro a continuación las líneas que cambiarían:

import { html, render, nothing } from "https://unpkg.com/lit-html";
/* ... */
const template = (bool) => html`<p>Status: ${ bool ? "Open" : nothing }</p>`;

Bucles en lit-html

Con lit-html es muy fácil hacer bucles con enfoque funcional. Observa el siguiente ejemplo, donde tenemos un array inventory con varios objetos del Monkey Island. Nuestra idea va a ser crear una lista HTML a partir de este array:

<div id="app"></div>

<script type="module">
  import { html, render } from "https://unpkg.com/lit-html";

  const app = document.querySelector("#app");
  const inventory = [
    "breath mints", "100% cotton t-shirt",
    "map", "shovel", "rubber-chicken"
  ];

  const li = (name) => html`<li>${name}</li>`;  // Every item
  const ol = (items) => html`<ol>${ items.map((item) => li(item)) }</ol>`;  // List
  render(ol(inventory), app);
</script>

En la parte inferior tenemos dos templates: li, que es una plantilla por cada item <li> de la lista, y ol, que es la lista <ol> en sí. Observa que lo que hacemos es un items.map() para recorrer todos los items del array, y llamar a la plantilla li pasándole cada uno de esos items. También nos damos cuenta que es posible llamar plantillas desde otras plantillas, anidándolas.

De esta forma, el resultado es una lista en base al array que recorremos. Cada vez que ejecutaramos render sucesivamente, se examinará el array inventory y se actualiza la lista de elementos, variando sólo los elementos que hayan cambiado.

La función render()

En la mayoría de los frameworks y librerías de Javascript de nueva generación se utilizan los llamados métodos render. No son más que funciones encargadas de actualizar el código o DOM de la página para dibujar la nueva estructura, actualizando los datos (si se requiere).

Por ejemplo, en el ejemplo del apartado anterior, si cambiaramos el array inventory y añadieramos un nuevo item, la función render es la que se encargaría de actualizar esos datos en la página, puesto que de lo contrario sólo se habrían actualizado en el array de datos.

Vamos a aplicar lo que hemos visto de lit-html en un WebComponent nativo, creando el clásico contador numérico, añadiendo varias novedades:

  • Crearemos un método de clase template() que creará la plantilla con sus eventos de click.
  • Crearemos los métodos de clase add() y sub() que sumarán o restarán el contador.
  • Crearemos un método de clase render() que es al que llamaremos para actualizar los datos.
<app-element></app-element>

<script type="module">
  import { html, render } from "https://unpkg.com/lit-html";

  customElements.define("app-element", class extends HTMLElement {
    constructor() {
      super();
      this.count = 0;
      this.attachShadow({ mode: "open" });
    }

    template() {
      return html`
        <p>${this.count}</p>
        <button @click=${() => this.add()}>+</button>
        <button @click=${() => this.sub()}>-</button>
      `;
    }

    add() {
      this.count++;
      this.render();  // Ha cambiado, renderizamos
    }

    sub() {
      this.count--;
      this.render();  // Ha cambiado, renderizamos
    }

    connectedCallback() {
      this.render();  // Inicializamos componente, renderizamos
    }

    render() {
      render(this.template(), this.shadowRoot, { eventContext: this });
    }
  });
</script>

Observa que el método de clase render() lo que es hace es llamar a la función render() de lit-html, pasándole la plantilla generada por nuestro componente y añadiendola a this.shadowRoot, que es el Shadow DOM de nuestro componente.

Por último, se añade un objeto de opciones, donde indicamos eventContent: this, para establecer que this es el contexto que estamos usando en los eventos con @ de lit-html. Con todo esto tendríamos una primera aproximación a un componente utilizando algunas características muy cómodas de lit-html.

Más adelante veremos como aplicar lo que hemos aprendido de lit-html en un componente con la librería Lit, el hermano mayor de lit-html. Incorpora algunos ayudantes para hacer nuestro desarrollo de WebComponents mucho más agradable, así como el uso de la reactividad, un concepto muy interesante y cómodo para los desarrolladores, que nos evitará estar pendientes de llamar al render() para actualizar nuestros datos.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev