Todos nós temos projetos em que não trabalharíamos. O código tornou-se incontrolável, o escopo evoluiu, correções rápidas aplicadas em cima de outras correções, e a estrutura desmoronou sob o seu peso de código spaghetti. A codificação pode ser um negócio desorganizado.

Os projectos beneficiam da utilização de módulos simples e independentes que têm uma única responsabilidade. O código modular é encapsulado, por isso há menos necessidade de se preocupar com a implementação. Desde que você saiba o que um módulo irá sair quando lhe for dado um conjunto de entradas, você não precisa necessariamente entender como ele atingiu esse objetivo.

A aplicação de conceitos modulares a uma única linguagem de programação é simples, mas o desenvolvimento web requer uma mistura diversificada de tecnologias. Os navegadores analisam HTML, CSS e JavaScript para renderizar o conteúdo, estilos e funcionalidades da página.

Eles nem sempre se misturam facilmente porque:

  • O código relacionado pode ser dividido entre três ou mais arquivos, e
  • Estilos globais e objetos JavaScript podem interferir uns com os outros de forma inesperada.

Estes problemas somam-se aos encontrados pelos tempos de execução do idioma, frameworks, bancos de dados e outras dependências utilizadas no servidor.

Confira nosso Guia em Vídeo sobre Componentes da Web

O que são componentes da Web?

Um Componente Web é uma forma de criar um bloco de código de responsabilidade única encapsulado que pode ser reutilizado em qualquer página.

Considere a tag HTML <video>. Dado um URL, um visualizador pode usar controles como play, pause, mover para trás, mover para frente, e ajustar o volume.

O estilo e a funcionalidade são fornecidos, embora você possa fazer modificações usando vários atributos e chamadas API JavaScript. Qualquer número de elementos <video> pode ser colocado dentro de outras tags, e eles não entram em conflito.

E se você precisar de sua própria funcionalidade personalizada? Por exemplo, um elemento que mostre o número de palavras na página? Não há tag HTML <wordcount> (ainda).

Frameworks como React e Vue.js permitem que os desenvolvedores criem componentes web onde o conteúdo, estilo e funcionalidade podem ser definidos em um único arquivo JavaScript. Estes resolvem muitos problemas de programação complexos, mas tenha isso em mente:

  • Você deve aprender como usar essa estrutura e atualizar seu código à medida que ele evolui.
  • Um componente escrito para uma estrutura raramente é compatível com outro.
  • As frameworks sobem e diminuem em popularidade. Você se tornará dependente dos caprichos e prioridades da equipe de desenvolvimento e dos usuários.
  • Componentes Web padrão podem adicionar funcionalidade de navegador, o que é difícil de conseguir apenas em JavaScript (como o Shadow DOM).

Felizmente, os conceitos populares introduzidos em bibliotecas e frameworks costumam se tornar padrões na web. Levou algum tempo, mas os componentes da Web já chegaram.

Uma breve história dos componentes da Web

Após muitas falsas partidas específicas de fornecedores, o conceito de componentes Web padrão foi introduzido pela primeira vez por Alex Russell na Conferência Fronteers em 2011. A biblioteca de polímeros do Google (um polifill baseado nas propostas atuais) chegou dois anos depois, mas as primeiras implementações não apareceram no Chrome e Safari até 2016.

Os fornecedores de navegadores demoraram a negociar os detalhes, mas os componentes Web foram adicionados ao Firefox em 2018 e ao Edge em 2020 (quando a Microsoft mudou para o motor Chromium).

Compreensivelmente, poucos desenvolvedores têm estado dispostos ou capazes de adotar componentes Web, mas finalmente atingimos um bom nível de suporte a navegador com APIs estáveis. Nem tudo é perfeito, mas elas são uma alternativa cada vez mais viável aos componentes baseados em framework.

Mesmo que você ainda não esteja disposto a despejar seu favorito, os componentes da Web são compatíveis com todas as frameworks, e as APIs serão suportadas nos próximos anos.

Repositórios de componentes Web pré-construídos estão disponíveis para que todos possam dar uma olhada:

… mas escrever o seu próprio código é mais divertido!

Este tutorial fornece uma introdução completa aos Componentes Web escritos sem uma estrutura JavaScript. Você aprenderá o que eles são e como adaptá-los para seus projetos web. Você vai precisar de algum conhecimento de HTML5, CSS e JavaScript.

Começando com componentes da Web

Componentes Web são elementos HTML personalizados tais como <hello-world></hello-world>. O nome deve conter um traço para nunca entrar em conflito com os elementos oficialmente suportados na especificação HTML.

Deve-se definir uma classe ES2015 para controlar o elemento. Pode ser chamado de qualquer coisa, mas o HelloWorld é uma prática comum. Ele deve estender a interface do HTMLElement, que representa as propriedades e métodos padrão de cada elemento HTML.

Nota: Firefox permite estender elementos HTML específicos tais como HTMLParagraphElement, HTMLImageElement, ou HTMLButtonElement. Isto não é suportado em outros navegadores e não permite que você crie um Shadow DOM.

Para fazer qualquer coisa útil, a classe requer um método chamado connectedCallback() que é invocado quando o elemento é adicionado a um documento:

class HelloWorld extends HTMLElement {

  // connect component
  connectedCallback() {
    this.textContent = 'Hello World!';
  }

}

Neste exemplo, o texto do elemento está definido para “Hello World”.

A classe deve ser registrada no CustomElementRegistry para defini-la como um manipulador para um elemento específico:

customElements.define( 'hello-world', HelloWorld );

O navegador agora associa o elemento <hello-world> à sua classe HelloWorld quando o seu JavaScript é carregado (por exemplo <script type="module" src="./helloworld.js"></script>).

Agora você tem um elemento personalizado!

Demonstração no CodePen

Este componente pode ser estilizado no CSS como qualquer outro elemento:

hello-world {
  font-weight: bold;
  color: red;
}

Adicionando atributos

Este componente não é benéfico, uma vez que o mesmo texto é emitido independentemente. Como qualquer outro elemento, nós podemos adicionar atributos HTML:

<hello-world name="Craig"></hello-world>

Isto pode anular o texto para que “Hello Craig!” seja exibido. Para conseguir isso, você pode adicionar uma função constructor() à classe HelloWorld, que é executada quando cada objeto é criado. Ele deve:

  1. chamar o método super() para inicializar o HTMLElement pai, e
  2. fazer outras inicializações. Neste caso, vamos definir a propriedade como name  que é definido como padrão de “World”:
class HelloWorld extends HTMLElement {

  constructor() {
    super();
    this.name = 'World';
  }

  // more code...

O seu componente só se preocupa com o atributo name. Uma propriedade observedAttributes() estática deve retornar um array de propriedades a observar:

// component attributes
static get observedAttributes() {
  return ['name'];
}

Um método attributeChangedCallback() é chamado quando um atributo é definido no HTML ou alterado usando JavaScript. É passado o nome da propriedade, o valor antigo e o novo valor:

// attribute change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return;
  this[ property ] = newValue;

}

Neste exemplo, apenas o nome da propriedade seria atualizado, mas você poderia adicionar propriedades adicionais conforme necessário.

Finalmente, você precisa ajustar a mensagem no método connectedCallback():

// connect component
connectedCallback() {

  this.textContent = `Hello ${ this.name }!`;

}

Demonstração no CodePen

Métodos do ciclo de vida

O navegador chama automaticamente seis métodos durante todo o ciclo de vida do estado de Componente Web. A lista completa é fornecida aqui, embora você já tenha visto os primeiros quatro nos exemplos acima:

constructor()

É chamado quando o componente é inicializado pela primeira vez. Ele deve chamar super() e pode definir qualquer padrão ou realizar outros processos de pré-renderização.

static observedAttributes()

Retorna um conjunto de atributos que o navegador irá observar.

attributeChangedCallback(propertyName, oldValue, newValue)

Chamado sempre que um atributo observado é alterado. Os definidos em HTML são passados imediatamente, mas o JavaScript pode modificá-los:

document.querySelector('hello-world').setAttribute('name', 'Everyone');

O método pode precisar acionar uma nova renderização quando isso acontecer.

connectedCallback()

Essa função é chamada quando o Componente Web é anexado a um Modelo de Objeto de Documento. Ele deve executar qualquer renderização necessária.

disconnectedCallback()

É chamado quando o Componente Web é removido de um Modelo de Objeto de Documento. Isso pode ser útil se você precisar fazer uma limpeza, como remover o estado armazenado ou abortar solicitações Ajax.

adoptedCallback()

Esta função é chamada quando um componente da Web é movido de um documento para outro. Você pode encontrar um uso para isso, embora eu tenha lutado para pensar em qualquer caso!

Como os componentes da web interagem com outros elementos

Componentes da Web oferece algumas funcionalidades únicas que você não encontrará em frameworks JavaScript.

O Shadow DOM

Embora o Componente Web que construímos acima funcione, ele não é imune a interferências externas, e o CSS ou JavaScript poderia modificá-lo. Da mesma forma, os estilos que você define para o seu componente podem vazar e afetar outros.

O Shadow DOM resolve este problema de encapsulamento anexando um DOM separado ao Componente Web:

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

O modo pode ser qualquer um dos dois:

  1. “open” — JavaScript na página externa pode acessar o Shadow DOM (usando Element.shadowRoot), ou
  2. “closed” — o Shadow DOM só pode ser acessado dentro do Componente Web.

O Shadow DOM pode ser manipulado como qualquer outro elemento do DOM:

connectedCallback() {

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

  shadow.innerHTML = `
    <style>
      p {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>

    <p>Hello ${ this.name }!</p>`;

}

O componente agora renderiza o texto “Hello” dentro de um elemento <p> e o estiliza. 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.

Demonstração no CodePen

Os estilos definidos para este componente da Web não podem afetar outros parágrafos da página ou mesmo outros <hello-world> componentes.

Note que o CSS :host selector pode estilizar o elemento externo <hello-world> de dentro do Componente Web:

:host {
  transform: rotate(180deg);
}

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

:host(.rotate90) {
  transform: rotate(90deg);
}

Modelos HTML

A definição de HTML dentro de um script pode tornar-se impraticável para componentes Web mais complexos. Um template permite-lhe definir um pedaço de HTML na sua página que o seu Componente Web pode usar. Isto tem vários benefícios:

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

Os templates são definidos em um < template> tag, e é prático atribuir um ID para que você possa referenciá-lo dentro da classe do componente. Este exemplo três parágrafos para exibir a mensagem “Hello”:

<template id="hello-world">

  <style>
    p {
      text-align: center;
      font-weight: normal;
      padding: 0.5em;
      margin: 1px 0;
      background-color: #eee;
      border: 1px solid #666;
    }
  </style>

  <p class="hw-text"></p>
  <p class="hw-text"></p>
  <p class="hw-text"></p>

</template>

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('hello-world').content.cloneNode(true);

O DOM pode ser modificado e adicionado diretamente ao Shadow DOM:

connectedCallback() {

  const

    shadow = this.attachShadow({ mode: 'closed' }),
    template = document.getElementById('hello-world').content.cloneNode(true),
    hwMsg = `Hello ${ this.name }`;

  Array.from( template.querySelectorAll('.hw-text') )
    .forEach( n => n.textContent = hwMsg );

  shadow.append( template );

}

Demonstração no CodePen

Modelos de slots

Os slots permitem-lhe personalizar um modelo. Presuma que queria usar o seu <hello-world> Web Component mas coloque a mensagem dentro de um <h1> cabeçalho no Shadow DOM. Você poderia escrever este código:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>

</hello-world>

(Note o atributo slot.)

Opcionalmente, você poderia querer adicionar outros elementos, como um outro parágrafo:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Os slots podem agora ser implementados dentro do seu template:

<template id="hello-world">

  <slot name="msgtext" class="hw-text"></slot>

  <slot></slot>

</template>

Um atributo de slot de elemento definido como “msgtext” (o <h1> ) é inserido no ponto onde há uma <slot> chamada “msgtext”. O <p> não tem um nome de slot atribuído, mas é usado no próximo disponível sem nome <slot>. Na verdade, o modelo torna-se:

<template id="hello-world">

  <slot name="msgtext" class="hw-text">
    <h1 slot="msgtext">Hello Default!</h1>
  </slot>

  <slot>
    <p>This text will become part of the component.</p>
  </slot>

</template>

Não é assim tão simples na realidade. Um elemento <slot> no Shadow DOM aponta para os elementos inseridos. Você só pode acessá-los localizando um <slot> então usando o assignedNodes() method para retornar um array de infantil interno. O método connectedCallback() atualizado:

connectedCallback() {

  const
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name }`;

  // append shadow DOM
  shadow.append(
    document.getElementById('hello-world').content.cloneNode(true)
  );

  // find all slots with a hw-text class
  Array.from( shadow.querySelectorAll('slot.hw-text') )

    // update first assignedNode in slot
    .forEach( n => n.assignedNodes()[0].textContent = hwMsg );

}

Demonstração no CodePen

Além disso, não é possível estilizar diretamente os elementos inseridos, embora você possa direcionar os slots específicos dentro do seu Componente Web:

<template id="hello-world">

  <style>
    slot[name="msgtext"] { color: green; }
  </style>

  <slot name="msgtext" class="hw-text"></slot>
  <slot></slot>

</template>

Os modelos de slots são um pouco incomuns, mas um benefício é que o seu conteúdo será mostrado se o JavaScript não funcionar. Este código mostra um título e parágrafo padrão que só são substituídos quando a classe Web Component é executada com sucesso:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Portanto, você poderia implementar alguma forma de melhoria progressiva – mesmo que seja apenas uma mensagem “Você precisa de JavaScript”!

O Shadow DOM Declarativa

Os exemplos acima constroem um Shadow DOM usando JavaScript. Essa continua sendo a única opção, mas está sendo desenvolvido um Shadow DOM experimental declarativo para o Chrome. Isto permite a renderização do lado do servidor e evita qualquer mudança de layout ou flashes de conteúdo não arquivado.

O seguinte código é detectado pelo analisador de HTML, que cria um Shadow DOM idêntico ao que você criou na última seção (você precisaria atualizar a mensagem conforme necessário):

<hello-world name="Craig">

  <template shadowroot="closed">
    <slot name="msgtext" class="hw-text"></slot>
    <slot></slot>
  </template>

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Esta funcionalidade não está disponível em nenhum browser, e não há garantias de que chegue ao Firefox ou Safari. Você pode saber mais sobre o Shadow DOM declarativo, e um polifill é simples, mas esteja ciente de que a implementação pode mudar.

Eventos Shadow DOM

O seu Componente Web pode anexar eventos a qualquer elemento do Shadow DOM tal como faria na página DOM, tal como ouvir eventos de clique em todas as crianças internas:

shadow.addEventListener('click', e => {

  // do something

});

A menos que você stopPropagation, o evento surgirá no DOM da página, mas será redirecionado. Isto faz com que pareça vir de seu elemento personalizado e não dos elementos que o compõem.

Usando componentes web em outras frameworks

Qualquer componente da Web que você criar irá funcionar em todas as frameworks JavaScript. Nenhum deles conhece ou se importa com elementos HTML – seu componente <hello-world> será tratado de forma idêntica a um <div> e colocado no DOM onde a classe será ativada.

custom-elements-everywhere.com fornece uma lista de frameworks e notas de componentes da Web. A maioria é totalmente compatível, embora o React.js tenha alguns desafios. É possível usar <hello-world> no JSX:

import React from 'react';
import ReactDOM from 'react-dom';
import from './hello-world.js';

function MyPage() {

  return (
    <>
      <hello-world name="Craig"></hello-world> 
    </>
  );

}

ReactDOM.render(<MyPage />, document.getElementById('root'));

… mas:

  • Reagir só pode passar tipos de dados primitivos para atributos HTML (não arrays ou objetos)
  • Reagir não pode ouvir eventos de componentes da Web, por isso você deve anexar manualmente os seus próprios manipuladores.

Críticas e problemas dos componentes web

Os componentes da Web melhoraram significativamente, mas alguns aspectos podem ser complicados de gerir.

Dificuldades de estilização

Componentes da Web de Estilo coloca alguns desafios, especialmente se você quiser substituir os estilos scoped. Há muitas soluções:

  1. Evite usar o Shadow DOM. Você pode anexar conteúdo diretamente ao seu elemento personalizado, embora qualquer outro JavaScript possa acidental ou maliciosamente alterá-lo.
  2. Use as classes :host. Como vimos acima, o CSS escopado pode aplicar estilos específicos quando uma classe é aplicada ao elemento personalizado.
  3. Confira as propriedades (variáveis) personalizadas do CSS. Propriedades personalizadas em cascata em Componentes Web, assim, se o seu elemento usa var(--my-color), você pode definir –--my-color em um recipiente externo (como :root), e ele será usado.
  4. Tire proveito das partes do Shadow DOM. O novo selector::part() pode estilizar um componente interno que tem um atributo de peça, ou seja <h1 part="heading"> dentro de um componente <hello-world> pode ser estilizado com o selector hello-world::part(heading).
  5. Passe em uma string de estilos. Você pode passá-los como um atributo para aplicar dentro de um bloco <style>.

Nenhum é ideal, e você precisará planejar cuidadosamente como outros usuários podem personalizar o seu componente da Web.

Entradas ignoradas

Qualquer <input>, <textarea>, ou <select> em seu Shadow DOM não está automaticamente associado com o formulário que o contém. Os primeiros usuários de componentes da web adicionaram campos ocultos à página DOM ou usaram a interface FormData para atualizar valores. Nenhum deles é particularmente prático e quebra o encapsulamento dos componentes da web.

A nova interface ElementInternals permite que um componente web se conecte a formulários a fim de definir valores e validade personalizados. Ele é implementado no Chrome, mas um polifilhe está disponível para outros navegadores.

Para demonstrar, você vai criar um componente básico <input-age name="your-age"></input-age>. A classe deve ter um valor estático de formAssociated value set true e, opcionalmente, um método formAssociatedCallback() pode ser chamado quando a forma externa é associada:

// <input-age> web component
class InputAge extends HTMLElement {

  static formAssociated = true;

  formAssociatedCallback(form) {
    console.log('form associated:', form.id);
  }

O construtor deve agora executar o método attachInternals(), que permite ao componente comunicar com o formulário e outro código JavaScript que deseja inspecionar o valor ou validação:

  constructor() {

    super();
    this.internals = this.attachInternals();
    this.setValue('');

  }

  // set form value

  setValue(v) {

    this.value = v;

    this.internals.setFormValue(v);

  }

O método ElementInternal setFormValue() define o valor do elemento para o formulário pai inicializado com uma string vazia aqui (também pode ser passado um objeto FormData com múltiplos pares nome/valor). Outras propriedades e métodos incluem:

  • form: o formulário pai
  • labels: um conjunto de elementos que rotulam o componente
  • Opções de API de Validação de Restrições, tais como willValidate, checkValidity, e validMessage

O método connectedCallback() cria um Shadow DOM como antes, mas também deve monitorar o campo para alterações, assim o setFormValue() pode ser executado:

  connectedCallback() {

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

    shadow.innerHTML = `
      <style>input { width: 4em; }</style>
      <input type="number" placeholder="age" min="18" max="120" />`;

    // monitor input values
    shadow.querySelector('input').addEventListener('input', e => {
      this.setValue(e.target.value);
    });

  }

Agora você pode criar um formulário HTML usando este Componente Web que atua de forma similar a outros campos de formulário:

<form id="myform">

  <input type="text" name="your-name" placeholder="name" />

  <input-age name="your-age"></input-age>

  <button>submit</button>

</form>

Funciona, mas reconhecidamente parece um pouco complicado.

Confira na demonstração no CodePen

Para mais informações, veja este artigo sobre melhores controles de formulários.

Resumo

Os componentes da Web têm lutado para ganhar tração e adoção em uma era em que as frameworks JavaScript têm crescido em estatura e capacidade. Se você vem de React, Vue ou Angular, os componentes da web podem parecer complexos e incômodos, especialmente se eles não possuem recursos como a vinculação de dados e o gerenciamento do estado.

Há questões a serem abordadas, mas o futuro dos componentes da web é brilhante. Eles são independentes da framework, leves, rápidos e podem implementar características que seriam impossíveis apenas em JavaScript.

Há dez anos, poucas pessoas teriam abordado um site sem jQuery, mas os fornecedores de navegadores pegaram as melhores peças e acrescentaram alternativas nativas (como o querySelector). O mesmo será válido para as frameworks JavaScript, e os componentes web são o primeiro passo provisório.

Você tem perguntas sobre como usar componentes web? Vamos falar sobre isso na seção de comentários!

Craig Buckler

Desenvolvedor web freelancer do Reino Unido, escritor e palestrante. Está na área há muito tempo e discursa sobre padrões e desempenho.