Do componente da Web para o elemento Lit

1. Introdução

Última atualização:10/08/2021

Componentes da Web

Os componentes da Web são um conjunto de APIs de plataforma da Web que permitem criar novas tags HTML encapsuladas, reutilizáveis e personalizadas para serem usadas em páginas e apps da Web. Os componentes e widgets personalizados, construídos com base nos padrões de componentes da Web, funcionarão em navegadores modernos e podem ser usados com qualquer biblioteca JavaScript ou estrutura que funcione com HTML.

O que é o Lit?

O Lit é uma biblioteca simples para criar componentes da Web rápidos e leves que funcionam em qualquer framework ou sem framework. Com o Lit, é possível criar componentes, aplicativos, sistemas de design e outros recursos compartilháveis.

O Lit fornece APIs para simplificar tarefas comuns dos componentes da Web, como gerenciamento de propriedades, atributos e renderização.

O que você vai aprender

  • O que é um componente da Web
  • Os conceitos dos Web Components
  • Como criar um componente da Web
  • O que são lit-html e LitElement
  • O que o Lit faz sobre um componente da Web

O que você vai criar

  • Um componente da Web baunilha (polegar para cima / negativo)
  • Um componente da Web baseado em Lit com "Gostei"/"Não gostei"

O que é necessário

  • Qualquer navegador moderno atualizado (Chrome, Safari, Firefox, Chromium Edge). Os componentes da Web funcionam em todos os navegadores modernos, e os polyfills estão disponíveis para o Microsoft Internet Explorer 11 e o Microsoft Edge que não usa cromo.
  • Conhecimento sobre HTML, CSS, JavaScript e Chrome DevTools.

2. Configuração e explorando o Playground

Como acessar o código

Ao longo deste codelab, você encontrará links para o playground do Lit, como este:

O Playground é um sandbox de código executado integralmente no navegador. Ele pode compilar e executar arquivos TypeScript e JavaScript, além de resolver automaticamente importações para módulos de nós. Por exemplo:

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

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

É possível fazer todo o tutorial no playground do Lit, usando esses checkpoints como pontos de partida. Se você estiver usando o VS Code, poderá usar esses checkpoints para fazer o download do código inicial de qualquer etapa, bem como usá-los para verificar seu trabalho.

Conheça a interface do playground do Lit

A barra de guias do seletor de arquivos é rotulada como Seção 1, a seção de edição de código como Seção 2, a visualização da saída como Seção 3 e o botão para recarregar a visualização comoSeção 4

A captura de tela da interface do playground do Lit destaca as seções que serão usadas neste codelab.

  1. Seletor de arquivos. Observe o botão de adição...
  2. Editor de arquivos.
  3. Visualização do código.
  4. Botão "Atualizar".
  5. Botão "Fazer download".

Configuração do VS Code (avançado)

Confira os benefícios de usar essa configuração do VS Code:

  • Verificação do tipo de modelo
  • Modelo Intellisense e preenchimento automático

Se você já tem o NPM, o VS Code (com o plug-in lit-plugin) instalado e sabe como usar esse ambiente, basta fazer o download e iniciar esses projetos da seguinte forma:

  • Pressione o botão de download.
  • Extraia o conteúdo do arquivo .tar em um diretório.
  • Instale um servidor de desenvolvimento que possa resolver especificadores de módulo básico (a equipe do Lit recomenda usar @web/dev-server)
  • Execute o servidor de desenvolvimento e abra o navegador (se você estiver usando @web/dev-server, use npx web-dev-server --node-resolve --watch --open)
    • Se você estiver usando o package.json de exemplo, use npm run serve.

3. Definir um elemento personalizado

Elementos personalizados

Os componentes da Web são uma coleção de quatro APIs nativas da Web. São eles:

  • Módulos ES
  • Elementos personalizados
  • Shadow DOM
  • Modelos HTML

Você já usou a especificação de módulos ES, que permite criar módulos JavaScript com importações e exportações carregadas na página com <script type="module">.

Como definir um elemento personalizado

A especificação de elementos personalizados permite que os usuários definam seus próprios elementos HTML usando JavaScript. Os nomes precisam ter um hífen (-) para diferenciá-los dos elementos nativos do navegador. Limpe o arquivo index.js e defina uma classe de elemento personalizado:

index.js

class RatingElement extends HTMLElement {}

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

Um elemento personalizado é definido pela associação de uma classe que estende HTMLElement a um nome de tag com hífen. A chamada para customElements.define instrui o navegador a associar a classe RatingElement ao tagName ‘rating-element'. Isso significa que todos os elementos no documento com o nome <rating-element> serão associados a essa classe.

Coloque um <rating-element> no corpo do documento e veja o que é renderizado.

index.html

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

Agora, observando a saída, você verá que nada foi renderizado. Isso é esperado, porque você não informou ao navegador como renderizar <rating-element>. Para confirmar se a definição do elemento personalizado foi concluída, selecione o <rating-element> nas ferramentas do Chrome Dev. seletor de elementos e, no console, chamando:

$0.constructor

O que deve resultar em:

class RatingElement extends HTMLElement {}

Ciclo de vida do elemento personalizado

Os elementos personalizados vêm com um conjunto de hooks de ciclo de vida. São eles:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

O constructor é chamado quando o elemento é criado pela primeira vez, por exemplo, chamando document.createElement(‘rating-element') ou new RatingElement(). O construtor é um bom lugar para configurar o elemento, mas é uma prática não recomendada realizar manipulações de DOM no construtor para o elemento "inicialização" de desempenho.

O connectedCallback é chamado quando o elemento personalizado é anexado ao DOM. Normalmente, é aqui que as manipulações iniciais do DOM acontecem.

O disconnectedCallback é chamado depois que o elemento personalizado é removido do DOM.

O attributeChangedCallback(attrName, oldValue, newValue) é chamado quando qualquer um dos atributos especificados pelo usuário muda.

O adoptedCallback é chamado quando o elemento personalizado é adotado de outro documentFragment no documento principal usando adoptNode, como em HTMLTemplateElement.

Renderizar DOM

Agora, volte para o elemento personalizado e associe um elemento DOM a ele. Defina o conteúdo do elemento quando ele for anexado ao 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);

No constructor, você armazena uma propriedade de instância chamada rating no elemento. No connectedCallback, você adiciona filhos do DOM ao <rating-element> para mostrar a classificação atual com os botões "Gostei" e "Não gostei".

4. Shadow DOM

Por que usar o Shadow DOM?

Na etapa anterior, você notará que os seletores na tag de estilo que você inseriu selecionam qualquer elemento de avaliação na página, bem como qualquer botão. Isso pode fazer com que os estilos vazem do elemento e selecionem outros nós que talvez você não pretenda aplicar. Além disso, outros estilos fora desse elemento personalizado podem involuntariamente estilizar os nós dentro do seu elemento personalizado. Por exemplo, tente colocar uma tag de estilo no cabeçalho do 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>

A saída precisa ter uma caixa de borda vermelha ao redor do período da classificação. Este é um caso trivial, mas a falta de encapsulamento de DOM pode resultar em problemas maiores para aplicativos mais complexos. É aí que entra o Shadow DOM.

Como anexar uma raiz de sombra

Anexe uma shadow Root ao elemento e renderize o DOM dentro dessa raiz:

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);

Ao atualizar a página, você vai notar que os estilos no documento principal não podem mais selecionar os nós dentro da raiz paralela.

Como você fez isso? Na connectedCallback, você chamou this.attachShadow, que anexa uma raiz paralela a um elemento. O modo open significa que o conteúdo de sombra pode ser inspecionado e que a raiz paralela também pode ser acessada usando this.shadowRoot. Observe também o componente Web no inspetor do Chrome:

A árvore de capa no inspetor do Chrome. Há um <rating-element> com a#shadow-root (open) como filha e o DOM anterior nessa shadowroot.

Agora você verá uma raiz paralela expansível que contém o conteúdo. Tudo dentro dessa raiz paralela é chamado de Shadow DOM. Se você selecionar o elemento "rating" nas Ferramentas para desenvolvedores do Chrome e chamar $0.children, perceberá que ele não retorna filhos. Isso ocorre porque o Shadow DOM não é considerado parte da mesma árvore do DOM que os filhos diretos, mas sim a Shadow Tree.

Light DOM

Um experimento: adicione um nó como filho direto de <rating-element>:

index.html

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

Atualize a página e você verá que esse novo nó DOM no Light DOM desse elemento personalizado não aparece na página. Isso ocorre porque o Shadow DOM tem recursos para controlar a forma como os nós do Light DOM são projetados no shadow DOM usando elementos <slot>.

5. Modelos HTML

Por que usar modelos?

O uso de innerHTML e strings literais de modelo sem limpeza pode causar problemas de segurança com a injeção de script. Anteriormente, os métodos incluíam o uso de DocumentFragments, mas eles também apresentam outros problemas, como carregamento de imagens e scripts executados quando os modelos são definidos, além de introduzir obstáculos para a reutilização. É aqui que o elemento <template> entra em cena. Os modelos oferecem DOM inerte, um método de alto desempenho para clonar nós e modelos reutilizáveis.

Como usar modelos

Em seguida, faça a transição do componente para usar modelos 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>

Aqui você moveu o conteúdo do DOM para uma tag de modelo no DOM do documento principal. Refatore a definição do 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 esse elemento, consulte o modelo, extraia o conteúdo dele e clone esses nós com templateContent.cloneNode, em que o argumento true executa um clone profundo. Em seguida, você inicializa o DOM com os dados.

Parabéns, agora você tem um Web Component! Infelizmente, ele ainda não faz nada. Em seguida, adicione algumas funcionalidades.

6. Como adicionar funcionalidades

Vinculações de propriedades

Atualmente, a única maneira de definir a nota no elemento de classificação é construir o elemento, definir a propriedade rating no objeto e colocá-la na página. Infelizmente, não é assim que os elementos HTML nativos costumam funcionar. Os elementos HTML nativos costumam ser atualizados com mudanças de propriedade e atributo.

Faça com que o elemento personalizado atualize a visualização quando a propriedade rating mudar, adicionando as seguintes linhas:

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;
}

Você adiciona um setter e um getter para a propriedade rating e depois atualiza o texto do elemento de avaliação se ele estiver disponível. Isso significa que, se você definir a propriedade de avaliação no elemento, a visualização será atualizada. faça um teste rápido no console das Ferramentas para Desenvolvedores.

Vinculações de atributos

Agora, atualize a visualização quando o atributo mudar. Isso é semelhante a uma entrada que atualiza a visualização quando você define <input value="newValue">. Felizmente, o ciclo de vida do componente Web inclui o attributeChangedCallback. Atualize a nota adicionando as seguintes linhas:

index.js

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

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

Para que o attributeChangedCallback seja acionado, é necessário definir um getter estático para RatingElement.observedAttributes which defines the attributes to be observed for changes. Depois, defina a classificação de forma declarativa no DOM. Faça um teste:

index.html

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

A classificação agora precisa ser atualizada de maneira declarativa.

Funcionalidade do botão

Agora só falta a funcionalidade dos botões. O comportamento desse componente deve permitir que o usuário forneça uma única classificação de voto positivo ou negativo e forneça um feedback visual ao usuário. Você pode implementar isso com alguns listeners de eventos e uma propriedade de reflexão, mas primeiro atualize os estilos para oferecer um feedback visual anexando as seguintes linhas:

index.html

<style>
...

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

No Shadow DOM, o seletor :host se refere ao nó ou elemento personalizado ao qual a Shadow Root está anexada. Nesse caso, se o atributo vote for "up", o botão "Gostei" ficará verde, mas se vote for "down", then it will turn the thumb-down button red. Agora, implemente a lógica para isso criando uma propriedade / atributo de reflexão para vote de forma semelhante a como você implementou rating. Comece com as propriedades setter e getter:

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;
}

Inicialize a propriedade da instância _vote com null no constructor e, no setter, verifique se o novo valor é diferente. Nesse caso, você ajusta a nota e, o mais importante, reflete o atributo vote de volta para o host com this.setAttribute.

Em seguida, configure a vinculação 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;
  }
}

Novamente, esse é o mesmo processo que você realizou com a vinculação do atributo rating. Adicione vote à observedAttributes e defina a propriedade vote no attributeChangedCallback. Por fim, adicione alguns listeners de eventos de clique para dar a funcionalidade dos botões.

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';
}

No constructor, você vincula alguns listeners de clique ao elemento e mantém as referências. Na connectedCallback, você detecta eventos de clique nos botões. No disconnectedCallback, você limpa esses listeners e, nos próprios listeners de clique, define vote corretamente.

Parabéns, agora você tem um componente Web com todos os recursos. tente clicar em alguns botões! O problema agora é que meu arquivo JS está alcançando 96 linhas, meu arquivo HTML 43 linhas e o código é bastante detalhado e essencial para um componente tão simples. É aí que entra o projeto Lit do Google.

7. HTML lit

Checkpoint do código

Por que lit-html

Em primeiro lugar, a tag <template> é útil e tem bom desempenho, mas não acompanha a lógica do componente, o que dificulta a distribuição do modelo com o restante da lógica. Além disso, a maneira como os elementos do modelo são usados inerentemente empreendem o código imperativo, o que, em muitos casos, leva a um código menos legível em comparação com os padrões de codificação declarativos.

É aí que entra o lit-html. Lit HTML é o sistema de renderização do Lit que permite escrever modelos HTML em JavaScript e, em seguida, renderizar e renderizar novamente esses modelos com eficiência com dados para criar e atualizar o DOM. É semelhante às populares bibliotecas JSX e VDOM, mas é executado de forma nativa no navegador e com muito mais eficiência em muitos casos.

Como usar Lit HTML

Em seguida, migre o componente da Web nativo rating-element para usar o modelo Lit, que usa literais de modelos com tag, que são funções que usam strings de modelo como argumentos com uma sintaxe especial. O Lit usa elementos de modelo em segundo plano para oferecer uma renderização rápida, além de alguns recursos de sanitização para garantir a segurança. Comece migrando o <template> em index.html para um modelo Lit adicionando um método render() ao webcomponent:

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);
  }
}

Também é possível excluir o modelo em index.html. Neste método de renderização, você define uma variável com o nome template e invoca a função literal do modelo com a tag html. Você também vai notar que realizou uma vinculação de dados simples dentro do elemento span.rating usando a sintaxe de interpolação literal do modelo de ${...}. Isso significa que, eventualmente, não será mais necessário atualizar esse nó de maneira imperativa. Além disso, você chama o método render iluminado, que renderiza de forma síncrona o modelo na raiz paralela.

Como migrar para sintaxe declarativa

Agora que você eliminou o elemento <template>, refatore o código para chamar o método render recém-definido. Você pode começar aproveitando a vinculação de listener de eventos do Lit para limpar o código do listener:

index.js

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

Os modelos do Lit podem adicionar um listener de eventos a um nó com a sintaxe de vinculação @EVENT_NAME. Nesse caso, você atualiza a propriedade vote sempre que esses botões são clicados.

Em seguida, limpe o código de inicialização do listener de eventos no constructor, no connectedCallback e no disconnectedCallback:

index.js

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

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

// remove disonnectedCallback and _onUpClick and _onDownClick

Foi possível remover a lógica do listener de clique dos três callbacks e até remover o disconnectedCallback completamente. Também foi possível remover todo o código de inicialização do DOM do connectedCallback, fazendo com que ele parecesse muito mais elegante. Isso também significa que você pode se livrar dos métodos de listener _onUpClick e _onDownClick.

Por fim, atualize os setters de propriedade para usar o método render. Assim, o DOM poderá ser atualizado quando as propriedades ou os atributos mudarem:

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();
}

Aqui, você removeu a lógica de atualização de DOM do setter rating e adicionou uma chamada para render do setter vote. Agora o modelo ficou muito mais legível, porque agora as vinculações e os listeners de eventos são aplicados.

Atualize a página para ter um botão de classificação funcionando que vai ficar assim quando o voto for pressionado.

Controle deslizante de classificação &quot;Gostei&quot; e &quot;Não gostei&quot; com um valor de 6 e o indicador de polegar para cima em verde

8. LitElement

Por que usar o LitElement

Ainda há alguns problemas com o código. Primeiro, se você mudar a propriedade ou o atributo vote, a propriedade rating poderá ser modificada, resultando na chamada de render duas vezes. Apesar das chamadas repetidas de renderização serem essencialmente um ambiente autônomo e eficiente, a VM JavaScript ainda gasta tempo chamando essa função duas vezes de maneira síncrona. Em segundo lugar, é tedioso adicionar novas propriedades e atributos, pois exige muito código boilerplate. É aí que entra a LitElement!

A LitElement é a classe de base do Lit para criar componentes da Web rápidos e leves que podem ser usados em frameworks e ambientes. Em seguida, confira o que o LitElement pode fazer por nós no rating-element mudando a implementação para usá-lo.

Como usar o LitElement

Comece importando e transformando a classe base LitElement em uma subclasse do pacote lit:

index.js

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

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

Importe LitElement, que é a nova classe de base para o rating-element. Em seguida, você mantém a importação html e, por fim, css, que nos permite definir literais de modelo com tag CSS para cálculos CSS, modelos e outros recursos em segundo plano.

Em seguida, mova os estilos do método de renderização para a folha de estilo estática do 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;
      }
    `;
  }
 ...

É aqui que a maioria dos estilos fica no Lit. O Lit usa esses estilos e usa recursos do navegador, como folhas de estilo construíveis, para fornecer tempos de renderização mais rápidos e transmiti-los pelo polyfill de componentes da Web em navegadores mais antigos, se necessário.

Lifecycle

O Lit introduz um conjunto de métodos de callback do ciclo de vida de renderização, além dos callbacks nativos do componente Web. Esses callbacks são acionados quando as propriedades do Lit declaradas são alteradas.

Para usar esse recurso, é preciso declarar estaticamente quais propriedades vão acionar o ciclo de vida da renderização.

index.js

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

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

Aqui, você define que rating e vote acionarão o ciclo de vida de renderização do LitElement, além de definir os tipos que serão usados para converter os atributos da string em propriedades.

<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>

Além disso, a flag reflect na propriedade vote vai atualizar automaticamente o atributo vote do elemento host que você acionou manualmente no setter vote.

Agora que você tem o bloco de propriedades estáticas, pode remover toda a lógica de atualização de atributos e propriedades. Isso significa que é possível remover os seguintes métodos:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (setters e getters)
  • vote (setters e getters, mas mantêm a lógica de mudança do setter)

Você mantém o constructor, além de adicionar um novo método de ciclo de vida da 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()

Aqui, basta inicializar rating e vote e mover a lógica setter do vote para o método de ciclo de vida willUpdate. O método willUpdate é chamado antes de render sempre que qualquer propriedade de atualização é alterada, porque o LitElement agrupa as mudanças de propriedade em lotes e torna a renderização assíncrona. Mudanças nas propriedades reativas (como this.rating) em willUpdate não vão acionar chamadas desnecessárias de ciclo de vida de render.

Por fim, render é um método de ciclo de vida do LitElement que exige o retornar de um modelo do 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>`;
}

Não é mais necessário verificar a raiz paralela nem chamar a função render importada anteriormente do pacote 'lit'.

Seu elemento será renderizado na visualização agora. clique!

9. Parabéns

Parabéns! Você criou um componente da Web do zero e o evoluiu para um LitElement.

O Lit é muito pequeno (menos de 5 KB + compactado com gzip), super rápido e muito divertido de usar código. É possível fazer componentes que serão consumidos por outros frameworks ou você pode criar apps completos com eles.

Agora você sabe o que é um componente da Web, como criá-lo e como o Lit facilita essa criação.

Checkpoint do código

Quer comparar seu código final com o nosso? Compare aqui.

Qual é a próxima etapa?

Confira alguns dos outros codelabs.

Leia mais

Community