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 da plataforma Web que permitem criar novas tags HTML personalizadas, reutilizáveis e encapsuladas para usar em páginas e apps da Web. Componentes e widgets personalizados criados com base nos padrões de componentes da Web funcionam em navegadores modernos e podem ser usados com qualquer biblioteca ou framework JavaScript 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 nenhum framework. Você pode criar componentes, aplicativos, sistemas de design e outros recursos compartilháveis usando o Lit.

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

O que você vai aprender

  • O que é um componente da Web
  • Os conceitos de componentes da Web
  • Como criar um componente da Web
  • O que são lit-html e LitElement
  • O que o Lit faz além de um componente da Web

O que você vai criar

  • Um componente da Web padrão de gostei / não gostei
  • Um componente da Web baseado em Lit com botões de "Gostei" e "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 não Chromium.
  • Conhecimento de HTML, CSS, JavaScript e Chrome DevTools.

2. Configuração e 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 totalmente no navegador. Ele consegue compilar e executar arquivos TypeScript e JavaScript, além de resolver automaticamente importações para módulos de nós, como:

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

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

Você pode seguir o tutorial completo no playground do Lit, usando esses checkpoints como pontos de partida. Caso esteja usando o VS Code, você poderá usar esses checkpoints para fazer o download do código inicial em qualquer etapa, além de usá-los para verificar seu trabalho.

Conhecendo a interface do playground do Lit

A barra de guias do seletor de arquivos está marcada como "Section 1", a seção de edição de código como "Section 2", a visualização da saída como "Section 3" e o botão de recarregamento da visualização como "Section 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 que há um 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
  • Preenchimento automático e intellisense do modelo

Se você já tem um NPM, o VS Code (com o plug-in lit-plugin) instalado e sabe como usar esse ambiente, basta fazer o download e dar início aos projetos desta 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 simples. 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 exemplo package.json, use npm run serve

3. Definir um elemento personalizado

Elementos personalizados

Os componentes da Web são um conjunto de quatro APIs da Web nativas. 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 os próprios elementos HTML usando JavaScript. Os nomes precisam conter 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 associando uma classe que estende HTMLElement a um nome de tag com hífens. A chamada para customElements.define informa ao navegador para 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, olhando para a saída, você vai notar que nada foi renderizado. Isso é esperado, porque você não informou ao navegador como renderizar <rating-element>. Para confirmar que a definição do elemento personalizado foi concluída, selecione o <rating-element> no seletor de elementos do Chrome DevTools e, no console, chame:

$0.constructor

que vai gerar:

class RatingElement extends HTMLElement {}

Ciclo de vida de elementos personalizados

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, ao chamar document.createElement(‘rating-element') ou new RatingElement(). O construtor é um bom lugar para configurar o elemento, mas geralmente é considerado uma prática inadequada fazer manipulações do DOM no construtor por motivos de desempenho de "inicialização" do elemento.

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

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

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

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

Renderizar DOM

Agora, volte ao elemento personalizado e associe algum 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, adicione filhos do DOM a <rating-element> para mostrar a classificação atual, junto com os botões "Gostei" e "Não gostei".

4. Shadow DOM

Por que usar o Shadow DOM?

Na etapa anterior, você notou que os seletores na tag de estilo inserida selecionam qualquer elemento de classificaçã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 você não pretende estilizar. Além disso, outros estilos fora desse elemento personalizado podem estilizar sem querer os nós dentro dele. 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 intervalo da classificação. Este é um caso trivial, mas a falta de encapsulamento do DOM pode resultar em problemas maiores para aplicativos mais complexos. É aí que entra o Shadow DOM.

Como anexar uma raiz paralela

Anexe uma raiz de sombra 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 shadow.

Como você fez isso? No connectedCallback, você chamou this.attachShadow, que anexa uma raiz de sombra a um elemento. O modo open significa que o conteúdo shadow pode ser inspecionado e torna a raiz shadow acessível também via this.shadowRoot. Confira também o componente da Web no inspetor do Chrome:

A árvore do DOM no inspetor do Chrome. Há um <rating-element> com um#shadow-root (open) como filho e o DOM de antes dentro desse shadowroot.

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

Light DOM

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

index.html

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

Atualize a página. Você vai notar que o novo nó DOM no DOM leve deste elemento personalizado não aparece na página. Isso acontece porque o Shadow DOM tem recursos para controlar como os nós do Light DOM são projetados no Shadow DOM usando elementos <slot>.

5. Modelos HTML

Por que usar modelos

Usar innerHTML e strings de modelo literal sem limpeza pode causar problemas de segurança com injeção de script. Métodos anteriores incluíam o uso de DocumentFragments, mas eles também vêm com outros problemas, como carregamento de imagens e execução de scripts quando os modelos são definidos, além de introduzir obstáculos para a reutilização. É aqui que entra o elemento <template>. Os modelos fornecem 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. Agora 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 de modelo, consulte o modelo, extraia o conteúdo dele e clone esses nós com templateContent.cloneNode, em que o argumento true realiza um clone completo. Em seguida, inicialize o DOM com os dados.

Parabéns, agora você tem um componente da Web! Infelizmente, ele ainda não faz nada. Então, adicione alguma funcionalidade.

6. Como adicionar funcionalidade

Vinculações de propriedade

No momento, a única maneira de definir a classificação no elemento de classificação é construir o elemento, definir a propriedade rating no objeto e colocá-lo na página. Infelizmente, não é assim que os elementos HTML nativos funcionam. Os elementos HTML nativos tendem a 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 de classificação e atualiza o texto do elemento de classificação, se disponível. Isso significa que, se você definir a propriedade de classificação no elemento, a visualização será atualizada. Faça um teste rápido no console das ferramentas de desenvolvimento.

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 da Web inclui o attributeChangedCallback. Atualize a classificação 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. Em seguida, 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 será atualizada de forma declarativa.

Funcionalidade do botão

Agora só falta a funcionalidade do botão. O comportamento desse componente precisa permitir que o usuário dê uma única classificação de voto positivo ou negativo e forneça feedback visual. É possível implementar isso com alguns listeners de eventos e uma propriedade de reflexão, mas primeiro atualize os estilos para dar 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 a que a raiz paralela 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 criando uma propriedade / atributo de reflexão para vote, semelhante a como você implementou rating. Comece com o setter e o getter da propriedade:

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

Você inicializa a propriedade de instância _vote com null no constructor e, no setter, verifica se o novo valor é diferente. Se for o caso, ajuste a classificação de acordo e, principalmente, reflita o atributo vote de volta ao host com this.setAttribute.

Em seguida, configure a vinculação de atributo:

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ê fez com a vinculação de atributo rating. Adicione vote ao observedAttributes e defina a propriedade vote no attributeChangedCallback. Por fim, adicione alguns listeners de eventos de clique para dar funcionalidade aos 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';
}

Na constructor, você vincula alguns listeners de clique ao elemento e mantém as referências por perto. No connectedCallback, você detecta eventos de clique nos botões. No disconnectedCallback, você libera memória desses listeners e, nos listeners de clique, define vote adequadamente.

Parabéns! Agora você tem um componente da Web completo. Clique em alguns botões para testar. O problema agora é que meu arquivo JS está chegando a 96 linhas, meu arquivo HTML a 43 linhas, e o código é bastante detalhado e imperativo para um componente tão simples. É aí que entra o projeto Lit do Google.

7. Lit-html

Checkpoint do código

Por que lit-html

Primeiro, a tag <template> é útil e eficiente, mas não é incluída com 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 de modelo são usados leva inerentemente a um código imperativo, que, em muitos casos, resulta em um código menos legível em comparação com padrões de programação declarativa.

É aí que entra o lit-html. O 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 dados para criar e atualizar o DOM de maneira eficiente. Ele é semelhante às bibliotecas JSX e VDOM conhecidas, mas é executado de forma nativa no navegador e com muito mais eficiência em muitos casos.

Usar Lit HTML

Em seguida, migre o rating-element de componente da Web nativo para usar o modelo do Lit, que usa literais de modelo com tag, que são funções que usam strings de modelo como argumentos com uma sintaxe especial. O Lit usa elementos de modelo internamente para oferecer renderização rápida e alguns recursos de limpeza para segurança. Comece migrando o <template> em index.html para um modelo do 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);
  }
}

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

Migrar para a sintaxe declarativa

Agora que você se livrou do elemento <template>, refatore o código para chamar o método render recém-definido. Comece usando a vinculação do 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 em constructor e connectedCallback e disconnectedCallback:

index.js

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

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

// remove disonnectedCallback and _onUpClick and _onDownClick

Você conseguiu remover a lógica do listener de clique de todos os três callbacks e até mesmo remover o disconnectedCallback por completo. Você também conseguiu remover todo o código de inicialização do DOM do connectedCallback, deixando-o 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ê conseguiu remover a lógica de atualização do DOM do setter rating e adicionou uma chamada para render do setter vote. Agora o modelo está muito mais legível, já que você pode ver onde as vinculações e os listeners de eventos são aplicados.

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

Controle deslizante de avaliação com gostei e não gostei com um valor de 6 e o botão de gostei na cor verde

8. LitElement

Por que o LitElement?

Alguns problemas ainda estão presentes no código. Primeiro, se você mudar a propriedade ou o atributo vote, isso poderá mudar a propriedade rating, o que resultará na chamada de render duas vezes. Apesar das chamadas repetidas de render serem essencialmente uma operação nula e eficientes, a VM JavaScript ainda gasta tempo chamando essa função duas vezes de forma síncrona. Em segundo lugar, é cansativo adicionar novas propriedades e atributos, já que isso exige muito código boilerplate. É aí que entra o LitElement.

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 criando uma subclasse da classe de base LitElement do pacote lit:

index.js

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

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

Você importa LitElement, que é a nova classe de base para rating-element. Em seguida, mantenha a importação html e, por fim, css, que permite definir literais de modelo marcados com CSS para matemática, modelos e outros recursos do CSS.

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 vai usar esses estilos e recursos do navegador, como folhas de estilo construíveis, para oferecer tempos de renderização mais rápidos e passar pelo polyfill de componentes da Web em navegadores mais antigos, se necessário.

Lifecycle

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

Para usar esse recurso, declare de forma estática quais propriedades vão acionar o ciclo de vida de 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 vão acionar o ciclo de vida de renderização do LitElement, além de definir os tipos que serão usados para converter os atributos de 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 renderização de atributos e propriedades. Isso significa que você pode remover os seguintes métodos:

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

Você mantém o constructor e adiciona um novo 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()

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

Por fim, render é um método de ciclo de vida do LitElement que exige que retornemos 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 shadow nem chamar a função render importada anteriormente do pacote 'lit'.

O elemento vai ser renderizado na visualização. Clique nele.

9. Parabéns

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

O Lit é muito pequeno (< 5 kb minificado + compactado), muito rápido e divertido de programar. Você pode criar componentes para serem usados por outros frameworks ou criar apps completos com ele.

Agora você sabe o que é um componente da Web, como criar um e como o Lit facilita a criação deles.

Checkpoint do código

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

Qual é a próxima etapa?

Confira outros codelabs!

Leia mais

Community