Componentes con LitElement

Si ya has echado un vistazo a la creación de WebComponents nativos, conoces conceptos como Shadow DOM y sabes crear y utilizar plantillas con lit-html, probablemente estes preparado para crear componentes con LitElement.

LitElement: WebComponents

¿Qué es LitElement?

LitElement es una librería de Google que facilita la creación de WebComponents nativos, reduciendo y simplificando la cantidad de código que hay que escribir, sin sacrificar rendimiento y con una sintaxis muy, muy cercana a WebComponents.

Además, LitElement no tiene dependencias (salvo lit-html), y el objetivo es mejorar la DX (experiencia de desarrollador) al crear WebComponents, sin «oscurecer» el aprendizaje del mismo, a la vez que se acelera la productividad.

Marcado HTML con LitElement

Básicamente, LitElement es una clase que extiende la funcionalidad de HTMLElement permitiendo utilizar mecánicas más modernas que nos permiten escribir menos código al hacer nuestros componentes y centrarnos en lo verdaderamente importante.

Así pues, lo primero que debemos hacer es importar la clase LitElement y la función html desde la librería lit-element. Al crear el componente, en lugar de extender el elemento HTMLElement (como hemos hecho hasta ahora) extendemos de LitElement y comenzamos a utilizar las características que incorpora:

<app-element></app-element>

<script type="module">
  import { LitElement, html } from 'https://unpkg.com/[email protected]?module';

  customElements.define("app-element", class extends LitElement {
    render() {
      return html`
        <div class="element">
          <p>Hello, <strong>friend</strong>!</p>
        </div>
      `;
    }
  });
</script>

Vamos a centrarnos en el método render(), que es el que crea el marcado HTML del componente. Utilizaremos la función html que vimos en el artículo de lit-html para crear el contenido del componente que automáticamente se actualizará en la página, aprovechando los beneficios del renderizado rápido y por partes de lit-html.

Básicamente con esto ya tendríamos un componente. Hemos escrito poco código, pero aún nos quedan bastantes cosas para hacerlo más potente, flexible y divertido.

Si lo necesitaramos, también podríamos añadir un constructor() para dotar de lógica de inicialización, como veníamos haciendo en los WebComponents nativos.

Estilos CSS con LitElement

El componente anterior fue muy sencillo de diseñar, pero no tiene estilos. Vamos a implementar el getter estático styles() para definir los estilos del componente. Importaremos la función css de LitElement, que no es más que un ayudante similar a html, pero en esta ocasión, para tratar los estilos CSS a través de un string template:

<app-element></app-element>

<script type="module">
  import { LitElement, html, css } from 'https://unpkg.com/[email protected]2.3.1?module';

  customElements.define("app-element", class extends LitElement {
    static get styles() {
      return css`
        .element {
          background: steelblue;
          color: white;
        }
        strong {
          background: orangered;
          padding: 4px;
        }
      `;
    }

    render() {
      return html`
        <div class="element">
          <p>Hello, <strong>friend</strong>!</p>
        </div>
      `;
    }
  });
</script>

Con esto, LitElement detectará los estilos creados y los aplicará automáticamente al componente. Ten en cuenta que por defecto, LitElement crea componentes con Shadow DOM de forma transparente al desarrollador, por lo que los estilos estarían aislados en el interior del componente.

En el caso de que no quisieramos utilizar un Shadow DOM sino un componente sencillo sin encapsulación (no suele ser lo habitual), tendríamos que añadir las siguientes líneas a la clase de nuestro componente:

createRenderRoot() {
  return this;
}

Devolviendo this con el método createRenderRoot() conseguiremos que no se cree un Shadow DOM, sino que devuelva el propio componente como raíz.

Reactividad con LitElement

En LitElement los eventos se manipulan exactamente igual que en el capítulo de lit-html. Vamos a modificar nuestro componente para que cuando hagamos click sobre el nombre, rote y cambie a otro nombre de una lista.

Sin embargo, lo más interesante de este ejemplo es el concepto de reactividad. A través del getter properties definimos las propiedades de clase que serán reactivas. Dicho de otra forma, cada vez que LitElement detecte que cambia alguna de estas propiedades reactivas, volverá a llamar internamente al método render para que se actualice con los nuevos datos.

Esto nos proporciona dos ventajas principales interesantes:

  • No tenemos que preocuparnos por llamar manualmente al render.
  • Al estar utilizando lit-html internamente, sólo se actualizarán las partes que cambian, haciendo la actualización prácticamente instantánea.

Pero veamos un ejemplo en funcionamiento:

<app-element></app-element>

<script type="module">
  import { LitElement, html, css } from 'https://unpkg.com/[email protected]?module';

  const NAMES = ["Manz", "Guybrush Threepwood", "Roger Wilco", "Rufus"];

  customElements.define("app-element", class extends LitElement {
    constructor() {
      super();
      this.nameSelected = 0;
    }

    static get properties() {
      return {
        nameSelected: { type: Number },
      };
    }

    static get styles() {
      return css`
        .element {
          background: steelblue;
          color: white;
        }
        strong {
          background: orangered;
          padding: 4px;
          user-select: none;
        }
      `;
    }

    nextName() {
      this.nameSelected = (this.nameSelected + 1) % NAMES.length;
    }

    render() {
      return html`
        <div class="element">
          <p>Hello, friend
            <strong @click=${this.nextName}>${NAMES[this.nameSelected]}</strong>!
          </p>
        </div>
      `;
    }
  });
</script>

La clave de este ejemplo está en observar la incorporación de la propiedad de clase nameSelected. Se trata de un número (índice) que nos indicará que nombre del array NAMES se mostrará en el texto del componente. Cada vez que pulsemos sobre ese nombre, activará a través de un evento el método nextName(), que irá rotando al siguiente ítem del array.

La propiedad nameSelected, al estar «vigilada» por properties, vuelve a disparar el método render() cuando cambia, actualizando las partes que han sido modificadas del componente, manteniendo siempre el aspecto visual actualizado respecto a sus datos.

En los siguientes artículos profundizaremos más en la gestión de CSS del componente, la definición de propiedades reactivas o la gestión del ciclo de vida de LitElement.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Autor de Emezeta.com, es profesor en la Universidad de La Laguna y dirige el curso de Programación web FullStack y Diseño web FrontEnd de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.