Executando verificação de segurança...
1

🧩 Web Components: principais conceitos e como implementá-los

Custom elements allow web developers to define new HTML tags, extend existing ones, and create reusable web components.
Os Web Components foram criados para dar a possibilidade para que os desenvolvedores criem também as suas próprias tags HTML, de responsábilidade única e encapsulados, podendo ser reutilizados ao longo de várias páginas de um site. Estes componentes podem extender funcionalidades das tags nativas de HTML, além de também poderem ser extendidos para a criação de outros componentes mais específicos. Quem conheçe tecnologias como o ReactJS por exemplo, vai notar uma certa familiaridade.

Como dar um nome

  • Possuir somente letras minúsculas;
  • Deve iniciar com uma letra;
  • Possuir no mínimo um hífen ("-");
  • Os únicos caracteres especiais possíveis são "-" e "_";

Como utilizar

  • Devem ser usados com tag de abertura e fechamento (não podem ser autocontidas, como a tag <img />, por exemplo);

Como implementar

  • O componente deve ser uma classe registrada em window.customElements;
  • Registrar a mesma tag duas vezes vai gerar uma DOMException.
  • O primeiro comando dentro do construtor é sempre super();

Hello World

Sabedores das regras, vamos criar, e utilizar o nosso primeiro web component. Verifique sempre no browser, e no devtools o resultado.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello World</title>
</head>
<body>
  <hello-world></hello-world>

  <script>
    class HelloWorld extends HTMLElement {
      constructor() {
        super();
        this.innerHTML = `<h1>Hello World</h1>`;
      }
    }

    window.customElements.define('hello-world', HelloWorld);
  </script>
</body>
</html>

Extendendo de outro componente (Hello User)

Agora vamos criar a classe HelloUser, que extende de HelloWorld, e adiciona o comportamento de receber o parâmetro name.
Se não receber parâmetro, exibirá Hello World pois este é o comportamento implementado no construtor da classe pai.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello World</title>
</head>
<body>
  <hello-world></hello-world>
  <hello-user></hello-user>
  <hello-user name="Belclei"></hello-user>

  <script>
    class HelloWorld extends HTMLElement {
      constructor() {
        super();
        this.innerHTML = `<h1>Hello World</h1>`;
      }
    }

    class HelloUser extends HelloWorld {
      static get observedAttributes() {
        return ['name']
      }
      attributeChangedCallback(name, oldValue, newValue) {
        if (newValue) {
          this.innerHTML = `<h1>Hello, ${newValue}</h1>`;
          return
        }
        this.innerHTML = '<h1>Hello World!</h1>'
      }
    }

    window.customElements.define('hello-user', HelloUser);
    window.customElements.define('hello-world', HelloWorld);
  </script>
</body>
</html>

O método estático observedAttributes sempre retorna um array com o nome dos atributos cuja alteração de valores será observada.
attributeChangedCallback é o callback que será disparado quando um dos atributos observados for alterado (nosso componente já começa a ficar reativo).

Expondo atributos

Para que possamos acessar diretamente os parâmetros via javascript, como seria o caso do atributo id (meuComponente.id), precisamos implementar setters e getters. No exemplo abaixo, além de separar arquivos HTML e JS, implementaremos o componente Hello User Desabilitável. Confira:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello World</title>
</head>
<body>
  <hello-world></hello-world>
  <hello-user></hello-user>
  <hello-user name="Belclei"></hello-user>
  <hello-user-disableable name="Fulano" disabled></hello-user-disableable>

  <script src="./js/hello-world.js"> </script>
  <script src="./js/hello-user.js"> </script>
  <script src="./js/hello-user-disableable.js"> </script>
</body>
</html>
class HelloUserDisableable extends HelloUser {
  static get observedAttributes() {
    return ['disabled', 'name'];
  }
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'disabled') {
      if (this.disabled) {
        this.setAttribute('tabindex', '-1');
        this.setAttribute('aria-disabled', 'true');
        this.style.color = '#DDD'
      } else {
        this.setAttribute('tabindex', '0');
        this.setAttribute('aria-disabled', 'false');
        this.style.color = ''
      }
    }
    if (name === 'name') {
      if (newValue) {
        this.innerHTML = `<h1>Hello, ${newValue}</h1>`;
        return
      }
      this.innerHTML = '<h1>Hello World!</h1>';
    }
  }
}
window.customElements.define('hello-user-disableable', HelloUserDisableable);

Perceba que criei o componente extendendo de HelloUser. Mas também poderia ser de HelloWorld, já que todos os métodos foram sobrescritos, com excessão do construtor.
Os métodos gete set são chamados sempre que o atributo é referenciado diretamente no JS (document.querySelector('hello-user-disableable').disabled), mas não quando sobre ele são executadas as funções getAttribute, ou removeAttribute, por exemplo.

Vamos agora criar um botão

Imagine que precisássemos criar um botão, e tivéssemos que reimplementar todo o comportamento que já existe em um botão nativo. Ainda bem que dá para extender um componente nativo. Infelizmente, até o momento, somente o Chrome já terminou de implementar esta funcionalidade .

class LogButton extends HTMLButtonElement {
  constructor() {
    super();
    this.addEventListener('click', e => console.log('button clicked'));
  }
}

customElements.define('log-button', LogButton, { extends: 'button' });
<button is="log-button">Clique Aqui</button>

Repare em dois detalhes. O primeiro é sobre o uso do terceiro parâmetro do customElements.define, que é um objeto que recebe a opção extends em que inserimos qual a tag que desejamos estender, e o segundo que não colocamos e sim usando o atributo is direto na tag button.

Métodos de ciclo de vida

  • constructor: chamado quando um novo elemento é instanciado ou aprimorado. Útil para definir estados iniciais, adicionar eventos e afins.
  • connectedCallback: chamado quando o elemento é inserido dentro do DOM. Útil para rodar código preparatório ou que receba alguma condição externa ao entrar no DOM.
  • disconnectedCallback: chamado quando o elemento é removido do DOM. Útil para rodar códigos de limpeza, como remoção de ouvintes de evento, e cancelamento de chamadas rest.
  • attributeChangedCallback(attrName, oldVal, newVal): chamando quando algum atributo observado é adicionado, alterado ou removido. Esses atributos são listados no array retornado no método getter estático observedAttributes. Esse é o único método que recebe parâmetros, sendo o primeiro o nome do atributo alterado, o segundo sendo o valor antigo do atributo e o último sendo o novo valor.
  • adoptedCallback: chamado quando o elemento é movido de documento. (por exemplo, quando alguém invoca document.adoptNode(el)). Somente encontrei um exemplo de implementação na internet, onde um elemento da DOM dentro de um iframe era transportada para a DOM da página principal.

Shadow DOM

Embora tenhamos criado nosso componente, ele não está isolado, ou imune a interferências externas. CSS ou Javascript podem alterar seu comportamento. O Shadow DOM foi criado para ser uma DOM separada, que pode ser anexada a um componente, resolvendo este problema de encapsulamento. A shadow DOM possui dois modos de funcionamento:

  • open: O JavaScript na página externa pode acessar o Shadow DOM (usando element.shadowRoot);
  • closed: o Shadow DOM só pode ser acessado dentro do Componente Web.

Vamos agora introduzir um novo componente para começar a aprender sobre essa separação, o shadow-hello. Seu conteúdo será uma cópia do componente hello-world. Primeiro vamos adicionar uma estilização no elemento <h1> e ver o que acontece:

    this.innerHTML = `<style>
      h1 {
        text-align: center;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>
    <h1>Hello World</h1>`;

Agora todos os elementos <h1> presentes na página estarão com a mesma estilização. Para evitar isso, vamos reimplementar o componente adicionando-o ao Shadow DOM:

class ShadowHello extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'closed' });
    shadow.innerHTML = `
    <style>
      h1 {
        text-align: center;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>
    <h1>Hello World!</h1>`;
  }

}
window.customElements.define('shadow-hello', ShadowHello);

Perceba que agora somente o componente shadow-hello possui a estilização. Ele não pode ser modificado por JavaScript ou CSS fora do componente, embora alguns estilos como a fonte e a cor sejam herdados da página porque não foram explicitamente definidos. E se passarmos a estilização para o HTML? Agora somente o shadow-hello NÃO é estilizado.
Você opode utilizar o seletor CSS :host para estilizar o elemento externo <shadow-hello> de dentro do Componente Web:

:host {
  color: red;
}

Você também pode definir estilos a serem aplicados quando o elemento usa uma classe específica, por exemplo <shadow-hello class="red">:

:host(.red) {
  color: red;
}

E se fosse necessário alterar o conteúdo interno de shadow-hello via JS? Como o componente foi criado com o modo closed isso é impossível.

const shadow = this.attachShadow({ mode: 'closed' });

Alterando o modo para open, você pode alterar o conteúdo interno do componente desta forma:

document.querySelector('shadow-hello').shadowRoot.innerHTML = '<h1>Novo conteúdo</h1';

Templates

A definição de HTML dentro do javascript pode tornar-se impraticável para componentes Web mais complexos, pois é tratado como string. A utilização de um template dá a possibilidade de definir um trecho de HTML na sua página que o seu web component pode usar. Isto tem vários benefícios:

  • Você pode ajustar o código HTML sem ter que reescrever as strings dentro do seu JavaScript.
  • Os componentes podem ser personalizados sem ter que criar classes JavaScript separadas para cada tipo.
  • É mais fácil definir HTML em HTML – e pode ser modificado no servidor ou cliente antes que o componente seja renderizado.

Neste exemplo implementamos o componente message-box, que utiliza o template com id box para colocar o conteúdo de seu innerHTML em um elemento com id message:

class MessageBox extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'closed' });
    const template = document.getElementById('box').content.cloneNode(true);
    template.querySelector('#message').innerHTML = this.innerHTML;
    shadow.append(template)
  }

}
window.customElements.define('message-box', MessageBox);

Os templates são definidos em um <template>, e é prático atribuir um ID para que você possa referenciá-lo dentro da classe do componente. Quando interpreta o HTML, o browser desconsidera os templates, e então, na execução do javascript seu conteúdo é adicionado na DOM. Perceba que o conteúdo de template só é exibido entre o título e o conteúdo da página:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Template Example</title>
</head>
<body>
  <template id="box">
    <style>
      div {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>
    <div>
      <span id="message"></span>
    </div>
  </template>

  <h1>Título</h1>
  <message-box>Texto da mensagem</message-box>
  <div>
    <p>Conteúdo da página.</p>
  </div>

  <script src="./message-box.js"></script>
</body>
</html>

A classe Web Component pode acessar este modelo, obter seu conteúdo e clonar os elementos para garantir que você esteja criando um fragmento único de DOM em todos os lugares onde ele é usado:

const template = document.getElementById('message-box').content.cloneNode(true);

Slots e Contents

As tags slot são espaços reservados durante a implementação de um componente, que serão ocupados posteriormente durante a sua utilização. O componente anterior foi alterado para não sobrescrever mais o innerHTML de nenhum componente.

class MessageBox extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'closed' });
    const template = document.getElementById('box').content.cloneNode(true);
    shadow.append(template)
  }

}
window.customElements.define('message-box', MessageBox);

No template foram adicionados dois slots, um para receber o conteúdo de message e outro para o title. Durante o uso do componente, no HTML, basta utilizar o nome do slot na propriedade slot:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Template Example</title>
</head>
<body>
  <template id="box">
    <style>
      div {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
      slot[name="title"] {
        color: red;
       }
    </style>
    <div>
      <slot name="title">Alert</slot>
      <slot name="message"></slot>
    </div>
  </template>

  <h1 slot="teste">Título</h1>
  <message-box>
    <strong slot="message">Texto da mensagem</strong>
    <h1 slot="title">Info</h1>
  </message-box>
  <div>
    <p>Conteúdo da página.</p>
  </div>

  <script src="./message-box.js"></script>
</body>
</html>

Perceba que o conteúdo original do slot "title" é "Alert". Esse é o valor padrão do slot. Caso ele não seja informado, este será o conteúdo exibido.

Referências

Carregando publicação patrocinada...
0

A próxima postagem que pretendo fazer 'se sobre stencil. Um framework para criação de web components com bastante inspiração no reactJS. Vocês estão interessados neste? Ou posso ir por outro caminho para trazer coisas úteis aqui. 😉