</> HTML5Advent
ENFRESDEITPT

// html · Web Platform Advent #5

Web Components: Custom Elements, Shadow DOM e templates

Crie elementos HTML reutilizáveis com as APIs nativas de Web Components — definir um Custom Element, encapsular estilos com o Shadow DOM, clonar um <template> e passar dados com atributos.

Uma pilha de coloridas peças de plástico encaixáveis de formas diferentes

Os Web Components são um conjunto de APIs nativas do navegador que lhe permitem construir os seus próprios elementos HTML reutilizáveis — sem necessidade de framework. Um componente que distribui funciona da mesma forma em HTML puro, React, Vue ou Svelte, porque é apenas um elemento que o navegador compreende. Três APIs tornam isto possível: os Custom Elements, o Shadow DOM e a tag <template>.

Custom Elements

Um Custom Element é uma classe que estende HTMLElement, registada com um nome de tag que tem de conter um hífen (para que nunca possa colidir com uma futura tag integrada):

class GreetingCard extends HTMLElement {
  connectedCallback() {
    const p = document.createElement('p');
    p.textContent = 'Hello from a custom element!';
    this.append(p);
  }
}

customElements.define('greeting-card', GreetingCard);

Uma vez definido, usa-o como qualquer outra tag:

<greeting-card></greeting-card>

A classe oferece-lhe callbacks de ciclo de vida. Os mais úteis são connectedCallback (o elemento foi adicionado à página), disconnectedCallback (foi removido) e attributeChangedCallback (um atributo observado mudou).

Reagir a atributos

Para passar dados, leia atributos — exatamente como src numa imagem. Declare quais observar com um getter estático observedAttributes, depois responda em attributeChangedCallback. Construir o nó com textContent mantém os valores fornecidos pelo utilizador em segurança:

class GreetingCard extends HTMLElement {
  static get observedAttributes() {
    return ['name'];
  }
  attributeChangedCallback(attr, oldVal, newVal) {
    if (attr === 'name') this.render();
  }
  connectedCallback() {
    this.render();
  }
  render() {
    const name = this.getAttribute('name') || 'friend';
    this.replaceChildren();
    const p = document.createElement('p');
    p.textContent = 'Hello, ' + name + '!';
    this.append(p);
  }
}
customElements.define('greeting-card', GreetingCard);

Agora <greeting-card name="Ada"></greeting-card> apresenta "Hello, Ada!", e alterar o atributo ao vivo volta a renderizá-lo.

An open metal toolbox with neatly organised tools in separate compartments
Um web component é como uma ferramenta numa caixa de ferramentas: uma unidade autónoma e identificada à qual recorre e reutiliza em muitos projetos sem a reconstruir de cada vez.

O Shadow DOM: encapsulamento real

O Shadow DOM dá a um elemento uma subárvore privada cujos estilos e markup estão isolados do resto da página. Um <style> no seu interior nunca pode vazar para fora, e o CSS externo nunca pode alcançá-lo por acidente. Construa-o com a API do DOM e um <slot> para conteúdo projetado:

class FancyButton extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent =
      'button { background:#2563eb; color:#fff; border:0;' +
      ' border-radius:8px; padding:.6rem 1rem; }';

    const button = document.createElement('button');
    button.append(document.createElement('slot'));

    shadow.append(style, button);
  }
}
customElements.define('fancy-button', FancyButton);

O <slot> é um marcador de posição: o que quer que coloque entre as tags — <fancy-button>Save</fancy-button> — é projetado nesse ponto, de modo que o componente envolve o seu conteúdo sem o substituir.

Templates: clone uma vez, reutilize a baixo custo

O elemento <template> contém markup inerte que é analisado mas não renderizado até que o clone. É a forma eficiente de estampar estruturas repetidas:

<template id="row-tpl">
  <li><span class="label"></span></li>
</template>

<script>
  const tpl = document.getElementById('row-tpl');
  const node = tpl.content.cloneNode(true);
  node.querySelector('.label').textContent = 'Item 1';
  document.querySelector('ul').append(node);
</script>

As três peças em conjunto

APIOferece-lhe
Custom ElementsAs suas próprias tags com callbacks de ciclo de vida
Shadow DOMEncapsulamento de estilo e markup, slots
<template>Markup inerte reutilizável e clonável

Raramente precisa dos três ao mesmo tempo. Um wrapper simples pode usar apenas um Custom Element; um widget estilizado e autónomo combina um Custom Element com o Shadow DOM e um slot. Como o resultado é DOM puro, integra-se em qualquer stack e sobrevive a qualquer framework que fosse popular no ano em que o escreveu.