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 plataforma web que te permiten crear nuevas etiquetas HTML personalizadas, reutilizables y encapsuladas para usarlas en páginas y apps web. Los componentes y widgets personalizados creados en los estándares de componentes web funcionarán en todos los navegadores modernos y pueden usarse con cualquier marco o biblioteca de JavaScript que funcione con HTML.

¿Qué es Lit?

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

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

Qué aprenderás

  • ¿Qué es un componente web?
  • Conceptos de 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 vanilla con Me gusta o No me gusta
  • Un componente web basado en Lit basado en Me gusta o No me gusta

Requisitos

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

2. Cómo prepararte y exploración de Playground

Accede al código

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

El Playground es una zona de pruebas de código que se ejecuta completamente en el navegador. Puede compilar y ejecutar archivos de TypeScript y JavaScript, y también puede resolver automáticamente 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 realizar 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 revisar tu trabajo.

Explora la IU de la zona de pruebas de Lit

La barra de pestañas del selector de archivos tiene la etiqueta Sección 1, la sección de edición de código como Sección 2, la vista previa de resultados como Sección 3 y el botón para volver a cargar la vista previa como 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. Observa el botón de 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:

  • Verificación del tipo de plantilla
  • Plantilla: intellisense autocompletado

Si tienes NPM y VS Code (con el complemento lit-plugin) ya instalados 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 tu navegador (si usas @web/dev-server, puedes usar npx web-dev-server --node-resolve --watch --open).
    • Si usas el ejemplo package.json, utiliza npm run serve.

3. Cómo definir un elemento personalizado

Elementos personalizados

Los componentes web 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 ES, que te permite crear módulos 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 Custom Elements les permite a los usuarios definir 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 guion. 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 elemento <rating-element> en el cuerpo del documento y observa qué se renderiza.

index.html

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

Si observas el resultado, verás que no se renderizó nada. Este es el comportamiento esperado, ya que no le indicaste al navegador cómo renderizar <rating-element>. Para confirmar que la definición del elemento personalizado se estableció correctamente, selecciona <rating-element> en las Herramientas para desarrolladores de Chrome. de elementos y, en la consola, llamar a lo siguiente:

$0.constructor

El resultado debería ser el siguiente:

class RatingElement extends HTMLElement {}

Ciclo de vida del elemento personalizado

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 generalmente no se considera una práctica recomendada realizar manipulaciones del DOM en el constructor para el "inicio" del elemento. por motivos de rendimiento.

Se llama a connectedCallback cuando el elemento personalizado se adjunta al DOM. Normalmente, aquí es donde ocurren 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.

DOM de renderización

Ahora, regresa al elemento personalizado y asócialo con algún DOM. Configura el contenido del elemento cuando se adjunte 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 Me gusta y No me gusta.

4. Shadow DOM

¿Por qué elegir Shadow DOM?

En el paso anterior, notarás que los selectores de la etiqueta de estilo que insertaste seleccionan cualquier elemento de calificación en la página, así como cualquier botón. Esto puede provocar que los estilos se filtren del elemento y se seleccionen otros nodos a los que tal vez no quieras aplicar ajustes de diseño. Además, otros diseños fuera de este elemento personalizado pueden definir el estilo de los nodos dentro de tu elemento personalizado de manera no intencional. Por ejemplo, prueba 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>

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

Cómo adjuntar una shadow root

Adjunta una Shadow Root 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 secundaria.

¿Cómo lo hiciste? En el connectedCallback, llamaste a this.attachShadow, que adjunta una shadow root a un elemento. El modo open significa que el contenido paralelo se puede inspeccionar y también permite que se pueda acceder a la shadow root mediante this.shadowRoot. Observa también el componente web en el inspector de Chrome:

El árbol de dom en el inspector de Chrome. Hay un elemento <rating-element> con un#shadow-root (abierto) como su campo secundario y el DOM de antes dentro de esa shadowroot.

Ahora, deberías ver una shadow root expandible que contiene el contenido. Todo lo que está dentro de esa shadow root se llama 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 muestra 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 como el Shadow Tree.

DOM claro

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 del DOM en el Light DOM del elemento personalizado no aparece en ella. Esto se debe a que el Shadow DOM tiene funciones para controlar cómo se proyectan los nodos del Light DOM en el shadow DOM mediante elementos <slot>.

5. Plantillas HTML

Por qué usar plantillas

El uso de innerHTML y strings literales de plantilla sin limpieza 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 también conlleva otros problemas, como la carga de imágenes y la ejecución de secuencias de comandos cuando se definen las plantillas, además de la introducción de obstáculos para la reutilización. Aquí es donde entra en juego el elemento <template>. Las plantillas proporcionan DOM inerte, un método de alto rendimiento para clonar nodos y plantillas reutilizables.

Cómo usar plantillas

A continuación, realiza la transición del componente para usar 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, debes consultarla, obtener su contenido y clonar los nodos con templateContent.cloneNode en los que el argumento true realiza una clonación profunda. Luego, inicializas el dominio con los datos.

Felicitaciones, ahora tienes un componente web. Lamentablemente, aún no hace nada, así que a continuación agrega algunas funcionalidades.

6. Agrega funciones

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. Desafortunadamente, no es así como los elementos HTML nativos suelen funcionar. Los elementos HTML nativos suelen actualizarse con cambios de propiedad y atributo.

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 método set y un método get 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á. realiza una prueba rápida en tu consola de Herramientas para desarrolladores.

Vinculaciones de atributos

Ahora, actualiza la vista cuando cambie el atributo. Esto es similar a una entrada que actualiza su vista cuando configuras <input value="newValue">. Por suerte, el ciclo de vida del componente web incluye attributeChangedCallback. Agrega las siguientes líneas para actualizar la calificación:

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 método get estático para RatingElement.observedAttributes which defines the attributes to be observed for changes. Luego, debes establecer la calificación de forma declarativa en el DOM. Pruebe lo siguiente:

index.html

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

Ahora la calificación debería actualizarse de forma declarativa.

Funcionalidad del botón

Ahora, lo único que falta es la funcionalidad del botón. El comportamiento de este componente debería permitirle al usuario proporcionar una calificación de un solo voto positivo o negativo, así como proporcionar comentarios visuales. Puedes implementar esto con algunos objetos de escucha de eventos y una propiedad de reflejo, pero primero actualiza los estilos 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 Shadow Root. En este caso, si el atributo vote es "up", el botón de Me gusta se volverá 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 reflectante para vote de manera similar a como implementaste rating. Comienza con el método set y el método get 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;
}

Inicializa la propiedad de la instancia _vote con null en constructor y, en el método set, 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 en el 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;
  }
}

Una vez más, este es el mismo proceso que realizaste con la vinculación de atributo rating. agregas vote a observedAttributes y estableces la propiedad vote en attributeChangedCallback. Y, ahora, por último, agrega algunos objetos de escucha de eventos de clic para darles la 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 el connectedCallback, escuchas los eventos de clic en los botones. En el disconnectedCallback, limpia estos objetos de escucha y, en los mismos objetos de clic, configura vote de manera correcta.

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

7. HTML de Lit

Punto de control del código

¿Por qué lit-html?

En primer lugar, la etiqueta <template> es útil y eficiente, pero no está empaquetada 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 la plantilla se presta de manera inherente al código imperativo, que, en muchos casos, conduce a un código menos legible en comparación con patrones de programación declarativos.

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

Cómo usar el HTML de Lit

A continuación, migra el componente web nativo rating-element para usar la plantilla de Lit que usa literales de plantillas etiquetados, que son funciones que toman strings de plantilla como argumentos con una sintaxis especial. Luego, Lit usa elementos de plantilla de forma interna para proporcionar una renderización rápida y algunas funciones de limpieza para la seguridad. Para comenzar, migra <template> en index.html a una plantilla de Lit. Para ello, agrega 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 tu plantilla de index.html. En este método de renderización, debes definir una variable llamada template y, luego, invocar la función literal de plantilla etiquetada html. También notarás que realizaste una vinculación de datos simple dentro del elemento span.rating con la sintaxis de interpolación literal de plantilla de ${...}. Esto significa que, con el tiempo, ya no necesitarás actualizar ese nodo de manera imperativa. Además, debes llamar al método render lit, que renderiza la plantilla de forma síncrona en la shadow root.

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 aprovechando la vinculación del objeto de escucha de eventos de lit para borrar 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, borra 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 también quitar el disconnectedCallback por completo. También pudiste quitar todo el código de inicialización del DOM de connectedCallback para que se vea mucho más elegante. Esto también significa que puedes deshacerte de los métodos de objeto de escucha _onUpClick y _onDownClick.

Por último, actualiza los establecedores de propiedades para usar el método render, de modo que el dominio 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 dominio del método set rating y agregaste una llamada a render desde el método set 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. Deberías ver un botón de calificación que funcione y debería verse de esta forma cuando se presione el voto a favor.

Control deslizante de calificación de Me gusta y No me gusta con valor 6 y el dedo hacia arriba de color verde

8. LitElement

Por qué LitElement

Aún hay algunos problemas en el código. Primero, si cambias la propiedad o el atributo vote, es posible que cambie la propiedad rating, lo que hará que se llame a render dos veces. A pesar de que las llamadas repetidas de renderización son, básicamente, una no-op y son eficientes, la VM de JavaScript aún pasa tiempo llamando a esa función dos veces de forma síncrona. En segundo lugar, es tedioso agregar propiedades y atributos nuevos, ya que requiere mucho código estándar. Aquí es donde LitElement entra en juego.

LitElement es la clase base de Lit para crear componentes web rápidos y ligeros que se pueden usar en frameworks y entornos. A continuación, observa lo que LitElement puede hacer por nosotros en rating-element cambiando la implementación para usarla.

Usa LitElement

Comienza por importar y subclasificar la clase base LitElement desde el paquete lit:

index.js

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

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

Importa LitElement, que es la nueva clase base del rating-element. A continuación, conservarás la importación de html y, por último, css, que nos permite definir literales de plantilla con etiquetas de CSS para las matemáticas de CSS, las plantillas y otros atributos de forma interna.

A continuación, mueve los estilos del método de renderización a la hoja de estilo 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 viven la mayoría de los estilos en Lit. Lit adoptará estos estilos y usará funciones del navegador, como Hojas de estilo constructibles, para proporcionar tiempos de renderización más rápidos y para pasarlo por el polyfill de componentes web en navegadores más antiguos, si es necesario.

Lifecycle

Lit presenta un conjunto de métodos de devolución de llamada de ciclo de vida de renderización además de las devoluciones de llamada de componentes web nativas. Estas devoluciones de llamada se activan cuando se cambian las propiedades declaradas de Lit.

Para usar esta función, debes declarar de forma estática qué propiedades activarán el ciclo de vida de la 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 activarán el ciclo de vida de renderización de LitElement y definirán 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 de la propiedad vote actualizará automáticamente el atributo vote del elemento de host que activaste manualmente en el método set vote.

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

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

Lo que conservas es el constructor y agregas un nuevo método de ciclo de vida de 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í, solo debes inicializar rating y vote, y mover la lógica del método set 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 agrupa 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 de ciclo de vida render innecesarias.

Por último, render es un método de ciclo de vida de LitElement que requiere que devuelvamos 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 shadow root y no debes llamar a la función render que se importó previamente desde el paquete 'lit'.

Tu elemento debería renderizarse en la vista previa ahora. ¡haz clic!

9. Felicitaciones

Felicitaciones, compilaste con éxito un componente web desde cero y lo convertiste en un LitElement.

Lit es muy pequeño (menos de 5 KB comprimidos y en formato gzip), súper rápido y muy divertido de programar. Puedes hacer que los componentes sean consumidos por otros frameworks o puedes compilar aplicaciones completas con ellos.

Ahora 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 algunos de los otros codelabs.

Lecturas adicionales

Comunidad