O Santo Graal dos componentes reutilizáveis: elementos personalizados, Shadow DOM e NPM
Publicados: 2022-03-10Mesmo para os componentes mais simples, o custo do trabalho humano pode ter sido significativo. As equipes de UX fazem testes de usabilidade. Uma série de partes interessadas tem que assinar o projeto.
Os desenvolvedores realizam testes AB, auditorias de acessibilidade, testes de unidade e verificações entre navegadores. Depois de resolver um problema, você não quer repetir esse esforço . Ao construir uma biblioteca de componentes reutilizáveis (em vez de construir tudo do zero), podemos utilizar continuamente os esforços anteriores e evitar revisitar os desafios de design e desenvolvimento já resolvidos.

Construir um arsenal de componentes é particularmente útil para empresas como o Google, que possuem um portfólio considerável de sites, todos compartilhando uma marca comum. Ao codificar sua interface do usuário em widgets que podem ser compostos, as empresas maiores podem acelerar o tempo de desenvolvimento e obter consistência de design visual e de interação do usuário em todos os projetos. Houve um aumento no interesse em guias de estilo e bibliotecas de padrões nos últimos anos. Dado vários desenvolvedores e designers espalhados por várias equipes, as grandes empresas buscam obter consistência. Podemos fazer melhor do que simples amostras de cores. O que precisamos é de código facilmente distribuível .
Compartilhando e reutilizando o código
Copiar e colar manualmente o código é fácil. Manter esse código atualizado, no entanto, é um pesadelo de manutenção. Muitos desenvolvedores, portanto, contam com um gerenciador de pacotes para reutilizar código entre projetos. Apesar do nome, o Node Package Manager tornou-se a plataforma incomparável para gerenciamento de pacotes front-end . Atualmente, existem mais de 700.000 pacotes no registro do NPM e bilhões de pacotes são baixados todos os meses. Qualquer pasta com um arquivo package.json pode ser carregada no NPM como um pacote compartilhável. Embora o NPM esteja associado principalmente ao JavaScript, um pacote pode incluir CSS e marcação. O NPM facilita a reutilização e, principalmente, a atualização do código. Em vez de precisar alterar o código em vários lugares, você altera o código apenas no pacote.
O problema da marcação
Sass e Javascript são facilmente portáveis com o uso de instruções de importação. As linguagens de modelagem dão ao HTML a mesma capacidade — os modelos podem importar outros fragmentos de HTML na forma de parciais. Você pode escrever a marcação para seu rodapé, por exemplo, apenas uma vez e incluí-la em outros modelos. Dizer que existe uma multiplicidade de linguagens de modelagem seria um eufemismo. Amarrar-se a apenas um limita severamente a potencial reutilização do seu código. A alternativa é copiar e colar a marcação e usar o NPM apenas para estilos e javascript.
Esta é a abordagem adotada pelo Financial Times com sua biblioteca de componentes Origami . Em sua palestra "Você não pode simplesmente torná-lo mais parecido com Bootstrap?" Alice Bartlett concluiu que “não há uma boa maneira de permitir que as pessoas incluam modelos em seus projetos”. Falando sobre sua experiência em manter uma biblioteca de componentes no Lonely Planet, Ian Feather reiterou os problemas com essa abordagem:
“Uma vez que eles copiam esse código, eles estão essencialmente cortando uma versão que precisa ser mantida indefinidamente. Quando eles copiavam a marcação para um componente de trabalho, havia um link implícito para um instantâneo do CSS naquele ponto. Se você atualizar o modelo ou refatorar o CSS, precisará atualizar todas as versões do modelo espalhadas pelo seu site.”
Uma Solução: Componentes Web
Os componentes da Web resolvem esse problema definindo marcação em JavaScript. O autor de um componente é livre para alterar marcação, CSS e Javascript. O consumidor do componente pode se beneficiar dessas atualizações sem precisar vasculhar um projeto alterando o código manualmente. A sincronização com as alterações mais recentes em todo o projeto pode ser alcançada com uma npm update
via terminal. Apenas o nome do componente e sua API precisam permanecer consistentes.
Instalar um componente web é tão simples quanto digitar npm install component-name
em um terminal. O Javascript pode ser incluído com uma instrução de importação:
<script type="module"> import './node_modules/component-name/index.js'; </script>
Então você pode usar o componente em qualquer lugar em sua marcação. Aqui está um componente de exemplo simples que copia texto para a área de transferência.
Veja a demonstração do componente web Pen Simple por CSS GRID (@cssgrid) no CodePen.
Uma abordagem centrada em componentes para o desenvolvimento front-end tornou-se onipresente, introduzida pela estrutura React do Facebook. Inevitavelmente, dada a difusão das estruturas em fluxos de trabalho front-end modernos, várias empresas construíram bibliotecas de componentes usando sua estrutura de escolha. Esses componentes são reutilizáveis apenas dentro dessa estrutura específica.

É raro uma empresa de porte ter um front-end uniforme e a mudança de plataforma de uma estrutura para outra não é incomum. Quadros vêm e vão. Para permitir a quantidade máxima de reutilização potencial entre projetos, precisamos de componentes que sejam independentes de estrutura .


“Eu construí aplicativos da web usando: Dojo, Mootools, Prototype, jQuery, Backbone, Thorax e React ao longo dos anos... aplicativo de hoje.”
— Dion Almaer, Diretor de Engenharia, Google
Quando falamos de um componente web, estamos falando da combinação de um elemento customizado com shadow DOM. Elementos personalizados e shadow DOM fazem parte da especificação W3C DOM e do padrão WHATWG DOM — o que significa que os componentes da web são um padrão da web . Elementos personalizados e shadow DOM estão finalmente definidos para obter suporte entre navegadores este ano. Ao usar uma parte padrão da plataforma da Web nativa, garantimos que nossos componentes possam sobreviver ao ciclo rápido de reestruturação de front-end e repensar a arquitetura. Os componentes da Web podem ser usados com qualquer linguagem de modelagem e qualquer estrutura de front-end — eles são realmente compatíveis e interoperáveis. Eles podem ser usados em qualquer lugar, desde um blog Wordpress até um aplicativo de página única.

Fazendo um componente web
Definindo um elemento personalizado
Sempre foi possível criar nomes de tags e fazer com que seu conteúdo aparecesse na página.

<made-up-tag>Hello World!</made-up-tag>
HTML é projetado para ser tolerante a falhas. O acima será renderizado, mesmo que não seja um elemento HTML válido. Nunca houve uma boa razão para fazer isso - desviar-se das tags padronizadas tem sido tradicionalmente uma má prática. Ao definir uma nova tag usando a API de elemento personalizado, no entanto, podemos aumentar o HTML com elementos reutilizáveis que possuem funcionalidade integrada. Criar um elemento personalizado é muito parecido com criar um componente no React — mas aqui estávamos estendendo HTMLElement
.
class ExpandableBox extends HTMLElement { constructor() { super() } }
Uma chamada sem parâmetros para super()
deve ser a primeira instrução no construtor. O construtor deve ser usado para configurar o estado inicial e os valores padrão e para configurar quaisquer ouvintes de eventos. Um novo elemento personalizado precisa ser definido com um nome para sua tag HTML e a classe correspondente aos elementos:
customElements.define('expandable-box', ExpandableBox)
É uma convenção para capitalizar nomes de classe. A sintaxe da tag HTML é, no entanto, mais do que uma convenção. E se os navegadores quisessem implementar um novo elemento HTML e quisessem chamá-lo de caixa expansível? Para evitar colisões de nomenclatura, nenhuma nova tag HTML padronizada incluirá um traço. Por outro lado, os nomes dos elementos personalizados devem incluir um traço.
customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid
Ciclo de vida do elemento personalizado
A API oferece quatro reações de elementos personalizados — funções que podem ser definidas dentro da classe que serão chamadas automaticamente em resposta a determinados eventos no ciclo de vida de um elemento personalizado.
connectCallback é executado quando o elemento personalizado é adicionado ao DOM.
connectedCallback() { console.log("custom element is on the page!") }
Isso inclui adicionar um elemento com Javascript:
document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”
além de simplesmente incluir o elemento dentro da página com uma tag HTML:
<expandable-box></expandable-box> // "custom element is on the page"
Qualquer trabalho que envolva buscar recursos ou renderização deve estar aqui.
DesconectadoCallback é executado quando o elemento personalizado é removido do DOM.
disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"
adoptedCallback
é executado quando o elemento personalizado é adotado em um novo documento. Você provavelmente não precisa se preocupar com isso com muita frequência.
attributeChangedCallback
é executado quando um atributo é adicionado, alterado ou removido. Ele pode ser usado para ouvir alterações em atributos nativos padronizados, como disabled ou src , bem como em quaisquer atributos personalizados que criamos. Este é um dos aspectos mais poderosos dos elementos personalizados, pois permite a criação de uma API amigável.
Atributos do elemento personalizado
Existem muitos atributos HTML. Para que o navegador não perca tempo chamando nosso attributeChangedCallback
quando qualquer atributo for alterado, precisamos fornecer uma lista das alterações de atributo que queremos ouvir. Para este exemplo, estamos interessados apenas em um.
static get observedAttributes() { return ['expanded'] }
Então agora nosso attributeChangedCallback
só será chamado quando alterarmos o valor do atributo expandido no elemento personalizado, pois é o único atributo que listamos.
Atributos HTML podem ter valores correspondentes (pense href, src, alt, value etc), enquanto outros são true ou false (por exemplo , disabled, selected, required ). Para um atributo com um valor correspondente, incluiríamos o seguinte na definição de classe do elemento personalizado.
get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }
Para nosso elemento de exemplo, o atributo será true ou false, portanto, definir o getter e o setter é um pouco diferente.
get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }
Agora que o clichê foi tratado, podemos usar attributeChangedCallback
.
attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }
Tradicionalmente, configurar um componente Javascript envolveria passar argumentos para uma função init
. Ao utilizar o attributeChangedCallback
, é possível criar um elemento personalizado configurável apenas com marcação.
Shadow DOM e elementos personalizados podem ser usados separadamente, e você pode achar elementos personalizados úteis por si só. Ao contrário do shadow DOM, eles podem ser preenchidos com polyfill. No entanto, as duas especificações funcionam bem em conjunto.
Anexando marcação e estilos com Shadow DOM
Até agora, lidamos com o comportamento de um elemento personalizado. No que diz respeito à marcação e estilos, no entanto, nosso elemento personalizado é equivalente a um <span>
sem estilo vazio. Para encapsular HTML e CSS como parte do componente, precisamos anexar um shadow DOM. É melhor fazer isso dentro da função construtora.
class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }
Não se preocupe em entender o que o modo significa - seu clichê você precisa incluir, mas você sempre desejará open
. Este componente de exemplo simples irá apenas renderizar o texto “hello world”. Como a maioria dos outros elementos HTML, um elemento personalizado pode ter filhos — mas não por padrão. Até agora, o elemento personalizado acima que definimos não renderizará nenhum filho na tela. Para exibir qualquer conteúdo entre as tags, precisamos fazer uso de um elemento slot
.
shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `
Podemos usar uma tag de estilo para aplicar algum CSS ao componente.
shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`
Esses estilos só se aplicam ao componente, portanto, podemos usar seletores de elementos sem que os estilos afetem mais nada da página. Isso simplifica a escrita de CSS, tornando desnecessárias convenções de nomenclatura como BEM.
Publicando um componente no NPM
Os pacotes NPM são publicados por meio da linha de comando. Abra uma janela de terminal e vá para um diretório que você gostaria de transformar em um pacote reutilizável. Em seguida, digite os seguintes comandos no terminal:
- Se o seu projeto ainda não tiver um package.json,
npm init
você na geração de um. -
npm adduser
vincula sua máquina à sua conta NPM. Se você não tiver uma conta pré-existente, ele criará uma nova para você. -
npm publish

Se tudo correu bem, agora você tem um componente no registro NPM, pronto para ser instalado e usado em seus próprios projetos — e compartilhado com o mundo.

A API de componentes da web não é perfeita. No momento, os elementos personalizados não podem incluir dados nos envios de formulários. A história do aprimoramento progressivo não é ótima. Lidar com acessibilidade não é tão fácil quanto deveria ser.
Embora anunciado originalmente em 2011, o suporte ao navegador ainda não é universal. O suporte ao Firefox está previsto para o final deste ano. No entanto, alguns sites de alto perfil (como o Youtube) já estão fazendo uso deles. Apesar de suas deficiências atuais, para componentes universalmente compartilháveis, eles são a opção singular e, no futuro, podemos esperar adições interessantes ao que eles têm a oferecer.