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.
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()
ysub()
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 delit-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 alrender()
para actualizar nuestros datos.