De un componente web a un elemento de Lit

1. Introducción

Última actualización: 10/08/2021

Componentes web

Los componentes web son un conjunto de APIs de la plataforma web que te permiten crear nuevas etiquetas HTML personalizadas, reutilizables y encapsuladas para usar en páginas y apps web. Los componentes y widgets personalizados compilados según los estándares de componentes web funcionarán en todos los navegadores modernos y se podrán usar con cualquier biblioteca o framework de JavaScript que funcione con HTML.

¿Qué es Lit?

Lit es una biblioteca simple para compilar componentes web rápidos y livianos que funcionan en cualquier framework o sin ningún framework. Con Lit, puedes compilar y compartir componentes, aplicaciones, sistemas de diseño y mucho más.

Lit proporciona APIs para simplificar tareas comunes de Web Components, como la administración de propiedades, atributos y la renderización.

Qué aprenderás

  • ¿Qué es un componente web?
  • Los conceptos de los componentes web
  • Cómo compilar un componente web
  • Qué son lit-html y LitElement
  • Qué hace Lit sobre un componente web

Qué compilarás

  • Un componente web de me gusta y no me gusta básico
  • Un componente web basado en Lit con un pulgar hacia arriba o hacia abajo

Requisitos

  • Cualquier navegador moderno actualizado (Chrome, Safari, Firefox, Chromium Edge) Los componentes web funcionan en todos los navegadores modernos, y hay polyfills disponibles para Microsoft Internet Explorer 11 y Microsoft Edge no basado en Chromium.
  • Conocimiento de HTML, CSS, JavaScript y Herramientas para desarrolladores de Chrome

2. Configura y explora la zona de pruebas

Cómo acceder al código

A lo largo del codelab, habrá vínculos a la zona de pruebas de Lit, como este:

La zona de pruebas es un entorno de pruebas de código que se ejecuta por completo en tu navegador. Puede compilar y ejecutar archivos de TypeScript y JavaScript, y también puede solucionar de manera automática las importaciones a módulos de nodos, p. ej.:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

Puedes completar todo el instructivo en la zona de pruebas de Lit y usar estos puntos de control como puntos de partida. Si usas VS Code, puedes usar estos puntos de control para descargar el código inicial de cualquier paso y usarlos para verificar tu trabajo.

Explora la IU de la zona de pruebas de Lit

La barra de la pestaña del selector de archivos se llama Sección 1, la sección de edición de código se denomina Sección 2, la vista previa de resultados se llama Sección 3 y el botón para volver a cargar la vista previa se llama Sección 4

La captura de pantalla de la IU de la zona de pruebas de Lit destaca las secciones que usarás en este codelab.

  1. Selector de archivos Presta atención al botón del signo más…
  2. Editor de archivos
  3. Vista previa del código
  4. Botón para volver a cargar
  5. Botón para descargar.

Configuración de VS Code (avanzada)

Estos son los beneficios de usar esta configuración de VS Code:

  • Comprobación del tipo de plantilla
  • Intellisense y autocompletado de plantillas

Si ya tienes instalado NPM y VS Code (con el complemento lit-plugin) y sabes cómo usar ese entorno, puedes descargar e iniciar estos proyectos de la siguiente manera:

  • Presiona el botón de descarga.
  • Extrae el contenido del archivo tar en un directorio.
  • Instala un servidor de desarrollo que pueda resolver especificadores de módulos básicos (el equipo de Lit recomienda @web/dev-server).
  • Ejecuta el servidor de desarrollo y abre el navegador (si usas @web/dev-server, puedes usar npx web-dev-server --node-resolve --watch --open).
    • Si usas el ejemplo package.json, usa npm run serve.

3. Cómo definir un elemento personalizado

Elementos personalizados

Los Web Components son una colección de 4 APIs web nativas. Son los siguientes:

  • Módulos de ES
  • Elementos personalizados
  • Shadow DOM
  • Plantillas HTML

Ya usaste la especificación de módulos de ES, que te permite crear módulos de JavaScript con importaciones y exportaciones que se cargan en la página con <script type="module">.

Cómo definir un elemento personalizado

La especificación de elementos personalizados permite que los usuarios definan sus propios elementos HTML con JavaScript. Los nombres deben contener un guion (-) para diferenciarlos de los elementos nativos del navegador. Borra el archivo index.js y define una clase de elemento personalizado:

index.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

Un elemento personalizado se define asociando una clase que extiende HTMLElement con un nombre de etiqueta con guiones. La llamada a customElements.define le indica al navegador que asocie la clase RatingElement con el tagName ‘rating-element'. Esto significa que cada elemento de tu documento con el nombre <rating-element> se asociará con esta clase.

Coloca un <rating-element> en el cuerpo del documento y observa cómo se renderiza.

index.html

<body>
 <rating-element></rating-element>
</body>

Ahora, si observas el resultado, verás que no se renderizó nada. Esto es normal, ya que no le indicaste al navegador cómo renderizar <rating-element>. Para confirmar que la definición del elemento personalizado se realizó correctamente, selecciona <rating-element> en el selector de elementos de las Herramientas para desarrolladores de Chrome y, en la consola, llama a:

$0.constructor

El resultado debería ser el siguiente:

class RatingElement extends HTMLElement {}

Ciclo de vida de los elementos personalizados

Los elementos personalizados incluyen un conjunto de hooks de ciclo de vida. Son los siguientes:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

Se llama a constructor cuando se crea el elemento por primera vez, por ejemplo, llamando a document.createElement(‘rating-element') o new RatingElement(). El constructor es un buen lugar para configurar tu elemento, pero, por lo general, se considera una mala práctica realizar manipulaciones del DOM en el constructor por motivos de rendimiento del "arranque" del elemento.

Se llama a connectedCallback cuando el elemento personalizado se adjunta al DOM. Aquí es donde suelen ocurrir las manipulaciones iniciales del DOM.

Se llama a disconnectedCallback después de que se quita el elemento personalizado del DOM.

Se llama a attributeChangedCallback(attrName, oldValue, newValue) cuando cambia cualquiera de los atributos especificados por el usuario.

Se llama a adoptedCallback cuando el elemento personalizado se adopta de otro documentFragment en el documento principal a través de adoptNode, como en HTMLTemplateElement.

Render DOM

Ahora, vuelve al elemento personalizado y asocia algo de DOM con él. Establece el contenido del elemento cuando se adjunta al DOM:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

En constructor, almacenas una propiedad de instancia llamada rating en el elemento. En connectedCallback, agregas elementos secundarios del DOM a <rating-element> para mostrar la calificación actual, junto con los botones de Me gusta y No me gusta.

4. Shadow DOM

¿Por qué usar Shadow DOM?

En el paso anterior, notarás que los selectores de la etiqueta de estilo que insertaste seleccionan cualquier elemento de calificación de la página, así como cualquier botón. Esto puede hacer que los estilos se filtren fuera del elemento y seleccionen otros nodos que no quieras diseñar. Además, otros estilos fuera de este elemento personalizado pueden aplicar estilo de forma no intencional a los nodos dentro de tu elemento personalizado. Por ejemplo, intenta colocar una etiqueta de estilo en el encabezado del documento principal:

index.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

El resultado debe tener un cuadro de borde rojo alrededor del intervalo de la calificación. Este es un caso trivial, pero la falta de encapsulación del DOM puede generar problemas más grandes en aplicaciones más complejas. Aquí es donde entra en juego Shadow DOM.

Cómo adjuntar una raíz secundaria

Adjunta una raíz de sombra al elemento y renderiza el DOM dentro de esa raíz:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

Cuando actualices la página, notarás que los estilos del documento principal ya no pueden seleccionar los nodos dentro de la raíz de Shadow.

¿Cómo lo hiciste? En el connectedCallback, llamaste a this.attachShadow, que adjunta una raíz de sombra a un elemento. El modo open significa que el contenido de la sombra se puede inspeccionar y que la raíz de la sombra también es accesible a través de this.shadowRoot. También puedes echar un vistazo al componente web en el inspector de Chrome:

El árbol del DOM en el inspector de Chrome. Hay un <rating-element> con un#shadow-root (open) como hijo y el DOM de antes dentro de ese shadowroot.

Ahora deberías ver una raíz de sombra expandible que contiene el contenido. Todo lo que se encuentra dentro de esa raíz de sombra se denomina Shadow DOM. Si seleccionas el elemento de calificación en las Herramientas para desarrolladores de Chrome y llamas a $0.children, notarás que no devuelve ningún elemento secundario. Esto se debe a que el Shadow DOM no se considera parte del mismo árbol del DOM que los elementos secundarios directos, sino del árbol de sombra.

DOM ligero

Un experimento: agrega un nodo como elemento secundario directo de <rating-element>:

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

Actualiza la página y verás que este nuevo nodo DOM en el DOM ligero de este elemento personalizado no aparece en la página. Esto se debe a que Shadow DOM tiene funciones para controlar cómo se proyectan los nodos de Light DOM en el Shadow DOM a través de elementos <slot>.

5. Plantillas HTML

Por qué usar plantillas

Usar innerHTML y cadenas de plantillas literales sin sanitización puede causar problemas de seguridad con la inyección de secuencias de comandos. En el pasado, los métodos incluían el uso de DocumentFragment, pero estos también presentan otros problemas, como la carga de imágenes y la ejecución de secuencias de comandos cuando se definen las plantillas, además de introducir obstáculos para la reutilización. Aquí es donde entra en juego el elemento <template>: las plantillas proporcionan un DOM inerte, un método de alto rendimiento para clonar nodos y plantillas reutilizables.

Cómo usar plantillas

A continuación, haz la transición del componente para que use plantillas HTML:

index.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

Aquí moviste el contenido del DOM a una etiqueta de plantilla en el DOM del documento principal. Ahora refactoriza la definición del elemento personalizado:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

Para usar este elemento de plantilla, consulta la plantilla, obtén su contenido y clona esos nodos con templateContent.cloneNode, donde el argumento true realiza una clonación profunda. Luego, inicializas el DOM con los datos.

¡Felicitaciones! Ahora tienes un componente web. Lamentablemente, aún no hace nada, así que, a continuación, agrega algunas funciones.

6. Cómo agregar funcionalidad

Vinculaciones de propiedades

Actualmente, la única forma de establecer la calificación en el elemento de calificación es construir el elemento, establecer la propiedad rating en el objeto y, luego, colocarlo en la página. Lamentablemente, los elementos HTML nativos no suelen funcionar de esta manera. Los elementos HTML nativos suelen actualizarse con los cambios de propiedades y atributos.

Para que el elemento personalizado actualice la vista cuando cambie la propiedad rating, agrega las siguientes líneas:

index.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

Agregas un setter y un getter para la propiedad de calificación y, luego, actualizas el texto del elemento de calificación si está disponible. Esto significa que, si estableces la propiedad de calificación en el elemento, la vista se actualizará. Pruébalo rápidamente en la consola de Herramientas para desarrolladores.

Vinculaciones de atributos

Ahora, actualiza la vista cuando cambie el atributo. Esto es similar a cuando una entrada actualiza su vista cuando estableces <input value="newValue">. Por suerte, el ciclo de vida de los componentes web incluye attributeChangedCallback. Actualiza la calificación agregando las siguientes líneas:

index.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

Para que se active attributeChangedCallback, debes establecer un getter estático para RatingElement.observedAttributes which defines the attributes to be observed for changes. Luego, estableces la calificación de forma declarativa en el DOM. Pruebe lo siguiente:

index.html

<rating-element rating="5"></rating-element>

La calificación ahora debería actualizarse de forma declarativa.

Funcionalidad del botón

Ahora, solo falta la funcionalidad del botón. El comportamiento de este componente debe permitir que el usuario proporcione una sola calificación de voto a favor o en contra, y que se le proporcione comentarios visuales. Puedes implementar esto con algunos objetos de escucha de eventos y una propiedad de reflejo, pero primero actualiza los diseños para proporcionar comentarios visuales agregando las siguientes líneas:

index.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

En Shadow DOM, el selector :host hace referencia al nodo o elemento personalizado al que se adjunta la raíz de sombra. En este caso, si el atributo vote es "up", el botón de Me gusta se pondrá de color verde, pero si vote es "down", then it will turn the thumb-down button red. Ahora, implementa la lógica para esto creando una propiedad o un atributo de reflejo para vote de manera similar a como implementaste rating. Comienza con el getter y el setter de la propiedad:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

Inicializas la propiedad de instancia _vote con null en el constructor y, en el setter, verificas si el valor nuevo es diferente. Si es así, ajusta la calificación según corresponda y, lo que es más importante, refleja el atributo vote al host con this.setAttribute.

A continuación, configura la vinculación de atributos:

index.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

Nuevamente, este es el mismo proceso que seguiste con la vinculación del atributo rating: agregas vote a observedAttributes y estableces la propiedad vote en attributeChangedCallback. Por último, agrega algunos objetos de escucha de eventos de clic para darles funcionalidad a los botones.

index.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

En el constructor, vinculas algunos objetos de escucha de clics al elemento y mantienes las referencias. En connectedCallback, escuchas los eventos de clic en los botones. En disconnectedCallback, limpias estos objetos de escucha y, en los objetos de escucha de clics, estableces vote de forma adecuada.

¡Felicitaciones! Ahora tienes un componente web con todas las funciones. Haz clic en algunos botones. El problema ahora es que mi archivo JS alcanza las 96 líneas, mi archivo HTML 43 líneas y el código es bastante detallado y obligatorio para un componente tan simple. Aquí es donde entra en juego el proyecto Lit de Google.

7. Lit-html

Punto de control del código

Por qué usar lit-html

En primer lugar, la etiqueta <template> es útil y eficiente, pero no se incluye en el paquete con la lógica del componente, lo que dificulta la distribución de la plantilla con el resto de la lógica. Además, la forma en que se usan los elementos de plantilla se presta inherentemente al código imperativo, lo que, en muchos casos, genera código menos legible en comparación con los patrones de codificación declarativa.

Aquí es donde entra en juego lit-html. Lit html es el sistema de renderización de Lit que te permite escribir plantillas HTML en JavaScript y, luego, renderizar y volver a renderizar de manera eficiente esas plantillas junto con los datos para crear y actualizar el DOM. Es similar a las bibliotecas populares de JSX y VDOM, pero se ejecuta de forma nativa en el navegador y es mucho más eficiente en muchos casos.

Cómo usar Lit HTML

A continuación, migra el componente web nativo rating-element para que use la plantilla de Lit, que usa literales de plantilla etiquetados, que son funciones que toman cadenas de plantilla como argumentos con una sintaxis especial. Luego, Lit usa elementos de plantilla de forma subyacente para proporcionar una renderización rápida y algunas funciones de limpieza para la seguridad. Para comenzar, migra el <template> en index.html a una plantilla de Lit agregando un método render() al componente web:

index.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

También puedes borrar la plantilla de index.html. En este método de renderización, defines una variable llamada template y, luego, invocas la función de literal de plantilla etiquetado html. También notarás que realizaste un simple vinculación de datos dentro del elemento span.rating con la sintaxis de interpolación literal de la plantilla de ${...}. Esto significa que, con el tiempo, ya no necesitarás actualizar ese nodo de forma imperativa. Además, llamas al método render de Lit, que renderiza de forma síncrona la plantilla en la raíz de sombreado.

Migra a la sintaxis declarativa

Ahora que te deshiciste del elemento <template>, refactoriza el código para llamar al método render recién definido. Puedes comenzar por aprovechar la vinculación del objeto de escucha de eventos de Lit para aclarar el código del objeto de escucha:

index.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

Las plantillas de Lit pueden agregar un objeto de escucha de eventos a un nodo con la sintaxis de vinculación @EVENT_NAME, en la que, en este caso, actualizas la propiedad vote cada vez que se hace clic en estos botones.

A continuación, limpia el código de inicialización del objeto de escucha de eventos en constructor y en connectedCallback y disconnectedCallback:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

Pudiste quitar la lógica del objeto de escucha de clics de las tres devoluciones de llamada y hasta quitar disconnectedCallback por completo. También pudiste quitar todo el código de inicialización del DOM de connectedCallback, lo que lo hace verse mucho más elegante. Esto también significa que puedes deshacerte de los métodos de escucha _onUpClick y _onDownClick.

Por último, actualiza los establecedores de propiedades para que utilicen el método render, de modo que el DOM se pueda actualizar cuando cambien las propiedades o los atributos:

index.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

Aquí, pudiste quitar la lógica de actualización del DOM del establecedor rating y agregaste una llamada a render desde el establecedor vote. Ahora, la plantilla es mucho más legible, ya que puedes ver dónde se aplican las vinculaciones y los objetos de escucha de eventos.

Actualiza la página y deberías tener un botón de calificación que funcione y que se vea así cuando se presione el voto positivo.

Control deslizante de calificación con pulgares hacia arriba y hacia abajo con un valor de 6 y el pulgar hacia arriba de color verde

8. LitElement

Por qué LitElement

Aún hay algunos problemas con el código. Primero, si cambias la propiedad o el atributo vote, es posible que cambie la propiedad rating, lo que provocará que se llame a render dos veces. A pesar de que las llamadas repetidas de renderización son esencialmente una operación nula y eficiente, la VM de JavaScript sigue dedicando tiempo a llamar a esa función dos veces de forma síncrona. En segundo lugar, es tedioso agregar propiedades y atributos nuevos, ya que se requiere mucho código estándar. Aquí es donde entra en juego LitElement.

LitElement es la clase base de Lit para crear componentes web rápidos y livianos que se pueden usar en todos los frameworks y entornos. A continuación, veamos qué puede hacer LitElement por nosotros en rating-element cambiando la implementación para usarlo.

Cómo usar LitElement

Comienza por importar y crear una subclase de la clase base LitElement del paquete lit:

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

Importas LitElement, que es la nueva clase base para rating-element. A continuación, mantén tu importación de html y, por último, css, lo que nos permite definir literales de plantilla etiquetados con CSS para operaciones matemáticas, plantillas y otras funciones de CSS en segundo plano.

A continuación, mueve los estilos del método de renderización a la hoja de diseño estática de Lit:

index.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

Aquí es donde residen la mayoría de los estilos en Lit. Lit tomará estos estilos y usará funciones del navegador, como las hojas de estilo constructibles, para proporcionar tiempos de renderización más rápidos, así como pasarlos por el polyfill de Web Components en navegadores más antiguos si es necesario.

Lifecycle

Lit introduce un conjunto de métodos de devolución de llamada del ciclo de vida de renderización sobre las devoluciones de llamada nativas de Web Components. Estas devoluciones de llamada se activan cuando cambian las propiedades de Lit declaradas.

Para usar esta función, debes declarar de forma estática qué propiedades activarán el ciclo de vida de renderización.

index.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

Aquí, defines que rating y vote también activarán el ciclo de vida de renderización de LitElement, además de definir los tipos que se usarán para convertir los atributos de cadena en propiedades.

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

Además, la marca reflect en la propiedad vote actualizará automáticamente el atributo vote del elemento host que activaste manualmente en el establecedor vote.

Ahora que tienes el bloque de propiedades estáticas, puedes quitar toda la lógica de actualización de la renderización de atributos y propiedades. Esto significa que puedes quitar los siguientes métodos:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (métodos get y set)
  • vote (métodos get y set, pero conserva la lógica de cambio del método set)

Lo que conservas es constructor, además de agregar un nuevo método de ciclo de vida willUpdate:

index.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

Aquí, simplemente inicializas rating y vote, y mueves la lógica del setter vote al método de ciclo de vida willUpdate. Se llama al método willUpdate antes de render cada vez que se cambia una propiedad de actualización, ya que LitElement procesa por lotes los cambios de propiedad y hace que la renderización sea asíncrona. Los cambios en las propiedades reactivas (como this.rating) en willUpdate no activarán llamadas innecesarias al ciclo de vida de render.

Por último, render es un método de ciclo de vida de LitElement que requiere que devolvamos una plantilla de Lit:

index.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

Ya no tienes que verificar la raíz sombreada ni llamar a la función render que importaste anteriormente del paquete 'lit'.

Ahora, el elemento debería renderizarse en la vista previa. Haz clic en él.

9. Felicitaciones

¡Felicitaciones! Compilaste correctamente un componente web desde cero y lo convertiste en un LitElement.

Lit es muy pequeño (menos de 5 KB comprimido y reducido), muy rápido y muy divertido para programar. Puedes crear componentes para que los usen otros frameworks o compilar apps completas con él.

Ahora ya sabes qué es un componente web, cómo crear uno y cómo Lit facilita su creación.

Punto de control del código

¿Quieres comparar tu código final con el nuestro? Compáralo aquí.

¿Qué sigue?

Consulta otros codelabs.

Lecturas adicionales

Comunidad