Elementos personalizados v1 - Componentes da Web reutilizáveis

Os elementos personalizados permitem que desenvolvedores da Web definam novas tags HTML, ampliem as já existentes e criem componentes da Web reutilizáveis.

Com os Elementos personalizados, os desenvolvedores da Web podem criar novas tags HTML, aprimorar as tags HTML existentes ou ampliar os componentes criados por outros desenvolvedores. A API é a base dos componentes da Web. Ela disponibiliza uma forma baseada em padrões da Web para criar componentes reutilizáveis usando nada mais que JS/HTML/CSS comuns. O resultado é menos código, código modular e mais reutilização nos apps.

Introdução

O navegador nos proporciona uma ferramenta excelente para estruturar aplicativos da Web. Ele se chama HTML. Talvez você já tenha ouvido falar dele. Ele é declarativo, portátil, tem suporte amplo e é fácil de trabalhar. O HTML é ótimo. No entanto, seu vocabulário e capacidade de extensão são limitados. O padrão atual do HTML sempre teve uma forma de associar automaticamente o comportamento do JS à marcação... até agora.

Os elementos personalizados são a resposta para a modernização do HTML, preenchendo as partes ausentes e agrupando estrutura e comportamento. Se o HTML não fornecer a solução para um problema, podemos criar um elemento personalizado que faça isso. Os elementos personalizados ensinam novas funcionalidades ao navegador, sem deixar de preservar os benefícios do HTML.

Como definir um novo elemento

Para definir um novo elemento do HTML, precisamos dos recursos do JavaScript.

O objeto global customElements é usado para definir um elemento personalizado e ensinar o navegador sobre uma nova tag. Chame customElements.define() com o nome da tag que você quer criar e um class do JavaScript que estende a HTMLElement de base.

Exemplo: definição de um painel de gaveta para dispositivos móveis, <app-drawer>:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

Exemplo de uso:

<app-drawer></app-drawer>

É importante lembrar que o uso de um elemento personalizado não é diferente do uso de um <div> ou qualquer outro elemento. As instâncias podem ser declaradas na página, criadas dinamicamente no JavaScript, é possível anexar ouvintes de eventos etc. Continue lendo para conferir mais exemplos.

Como definir a API JavaScript de um elemento

A funcionalidade de um elemento personalizado é definida usando um ES2015 class que estende HTMLElement. A extensão de HTMLElement garante que o elemento personalizado herde toda a API DOM e significa que todas as propriedades/métodos adicionados à classe passam a fazer parte da interface DOM do elemento. Use a classe para criar uma API JavaScript pública para sua tag.

Exemplo: definição da interface DOM de <app-drawer>:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

Neste exemplo, criamos uma gaveta que tem uma propriedade open, uma propriedade disabled e um método toggleDrawer(). Ele também reflete propriedades como atributos do HTML.

Um recurso elegante dos elementos personalizados é que this dentro de uma definição de classe se refere ao próprio elemento DOM, ou seja, à instância da classe. No nosso exemplo, this se refere a <app-drawer>. É assim (😉) que o elemento pode anexar um ouvinte click a si mesmo. E você não está limitado a listeners de eventos. Toda a DOM API está disponível dentro do código do elemento. Use this para acessar as propriedades do elemento, inspecionar os filhos (this.children), consultar nós (this.querySelectorAll('.items')) etc.

Regras para a criação de elementos personalizados

  1. O nome de um elemento personalizado precisa conter um hífen (-). Portanto, <x-tags>, <my-element> e <my-awesome-app> são nomes válidos, enquanto <tabs> e <foo_bar> não são. O motivo desse requisito é que o analisador HTML possa distinguir elementos personalizados de elementos normais. Além disso, garante compatibilidade futura quando novas tags são adicionadas ao HTML.
  2. Não é possível registrar a mesma tag mais de uma vez. Se você tentar fazer isso, será gerada uma DOMException. Informe o navegador sobre uma nova tag. É só isso. Não é possível cancelar.
  3. Os elementos personalizados não podem se fechar automaticamente, porque o HTML permite que alguns elementos se fechem automaticamente. Sempre escreva uma tag de fechamento (<app-drawer></app-drawer>).

Reações de elementos personalizados

Um elemento personalizado pode definir ganchos de ciclo de vida especiais para a execução de código durante momentos interessantes de sua existência. Elas são chamadas de reações de elementos personalizados.

Nome Chamado quando
constructor Uma instância do elemento é criada ou atualizada. Útil para inicializar o estado, configurar listeners de eventos ou criar um shadow dom. Consulte a especificação para ver as restrições sobre o que é possível fazer no constructor.
connectedCallback É chamado sempre que o elemento é inserido no DOM. Útil para executar código de configuração, como recuperar recursos ou renderizar. Normalmente, você deve tentar retardar o trabalho até esse momento.
disconnectedCallback Chamada sempre que o elemento é removido do DOM. Útil para executar código de limpeza.
attributeChangedCallback(attrName, oldVal, newVal) É chamado quando um atributo observado foi adicionado, removido, atualizado ou substituído. Também chamado para valores iniciais quando um elemento é criado pelo analisador ou atualizado. Observação:apenas atributos listados na propriedade observedAttributes vão receber esse callback.
adoptedCallback O elemento personalizado foi movido para um novo document (por exemplo, alguém chamado document.adoptNode(el)).

Os callbacks de reação são síncronos. Se alguém chamar el.setAttribute() no elemento, o navegador vai chamar attributeChangedCallback() imediatamente. Da mesma forma, você vai receber um disconnectedCallback() logo após o elemento ser removido do DOM (por exemplo, o usuário chama el.remove()).

Exemplo:adicionar reações de elemento personalizado a <app-drawer>:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

Defina reações se/quando isso fizer sentido. Se o elemento for bastante complexo e abrir uma conexão ao IndexedDB em connectedCallback(), faça o trabalho de limpeza necessário em disconnectedCallback(). Mas tome cuidado. Não é possível depender do elemento sendo removido do DOM em todas as circunstâncias. Por exemplo, disconnectedCallback() nunca será chamado se o usuário fechar a guia.

Propriedades e atributos

Como refletir propriedades em atributos

É comum que as propriedades do HTML reflitam seu valor no DOM como um atributo do HTML. Por exemplo, quando os valores de hidden ou id são alterados em JS:

div.id = 'my-id';
div.hidden = true;

Os valores são aplicados ao DOM em execução como atributos:

<div id="my-id" hidden>

Isso é chamado de refletir propriedades para atributos. Praticamente todas as propriedades do HTML fazem isso. Por quê? Os atributos também são úteis para configurar um elemento de forma declarativa, e algumas APIs, como acessibilidade e seletores CSS, dependem desses atributos para funcionar.

A reflexão de uma propriedade é útil sempre que você quiser manter a representação do DOM do elemento em sincronia com o estado do JavaScript. Um motivo para refletir uma propriedade é aplicar estilos definidos pelo usuário quando o estado do JS muda.

Lembre-se de nossa <app-drawer>. Um consumidor desse componente pode querer esmaecê-lo e/ou evitar interações do usuário enquanto o componente estiver desativado:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

Quando a propriedade disabled é alterada no JS, queremos que esse atributo seja adicionado ao DOM para que o seletor do usuário corresponda. O elemento pode fornecer esse comportamento refletindo o valor para um atributo do mesmo nome:

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

Observar mudanças nos atributos

Os atributos HTML são uma forma conveniente de declaração do estado inicial pelos usuários:

<app-drawer open disabled></app-drawer>

Os elementos podem reagir às mudanças de atributos definindo um attributeChangedCallback. O navegador vai chamar esse método para cada mudança nos atributos listados na matriz observedAttributes.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

No exemplo, estamos definindo atributos adicionais no <app-drawer> quando um atributo disabled é alterado. Embora não façamos isso aqui, você também pode usar o attributeChangedCallback para manter uma propriedade do JS sincronizada com o atributo.

Upgrades de elementos

HTML aprimorado progressivamente

Já aprendemos que os elementos personalizados são definidos chamando customElements.define(). Mas isso não significa que você tem de definir e registrar um elemento personalizado de uma só vez.

Os elementos personalizados podem ser usados antes do registro da definição.

O aprimoramento progressivo é um recurso dos elementos personalizados. Em outras palavras, você pode declarar vários elementos <app-drawer> na página e nunca invocar customElements.define('app-drawer', ...) até muito depois. Isso acontece porque o navegador trata possíveis elementos personalizados de forma diferente graças a tags desconhecidas. O processo de chamar define() e aprimorar um elemento existente com uma definição de classe é chamado de "atualizações de elementos".

Para saber quando um nome de tag é definido, use window.customElements.whenDefined(). Ele retorna uma promessa que é resolvida quando o elemento é definido.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

Exemplo: atrasar o trabalho até que um conjunto de elementos filhos seja atualizado

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

Conteúdo definido pelo elemento

Os elementos personalizados podem gerenciar o próprio conteúdo usando as APIs DOM dentro do código do elemento. As reações são úteis para isso.

Exemplo: crie um elemento com algum HTML padrão:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

A declaração dessa tag vai gerar:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// TODO: DevSite - O exemplo de código foi removido porque usava manipuladores de eventos inline.

Criar um elemento que usa o Shadow DOM

O Shadow DOM permite que um elemento tenha, renderize e estilize um bloco de DOM separado do resto da página. Você pode até ocultar todo um app em uma única tag:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

Para usar o Shadow DOM em um elemento personalizado, chame this.attachShadow dentro do constructor:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

Exemplo de uso:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

Texto personalizado do usuário

// TODO: DevSite - O exemplo de código foi removido porque usava manipuladores de eventos inline.

Como criar elementos usando um <template>

Para quem não conhece, o elemento <template> permite declarar fragmentos do DOM que são analisados, ficam inertes na carga da página e podem ser ativados posteriormente em tempo de execução. É outro primitivo de API na família de componentes da Web. Os modelos são um marcador ideal para declarar a estrutura de um elemento personalizado.

Exemplo:registrar um elemento com conteúdo do Shadow DOM criado usando um <template>:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

Essas poucas linhas de código fazem muita coisa. Vamos entender as principais atividades em andamento:

  1. Estamos definindo um novo elemento no HTML: <x-foo-from-template>
  2. O Shadow DOM do elemento é criado usando um <template>
  3. O DOM do elemento é local em relação ao elemento, graças ao Shadow DOM
  4. O CSS interno do elemento tem o escopo do elemento, graças ao Shadow DOM

Estou no Shadow DOM. Minha marcação foi copiada de um <template>.

// TODO: DevSite - O exemplo de código foi removido porque usava manipuladores de eventos inline.

Aplicar um estilo a um elemento personalizado

Mesmo que o elemento defina seu próprio estilo usando o Shadow DOM, os usuários podem estilizar o elemento personalizado na página. Eles são chamados de "estilos definidos pelo usuário".

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

Você pode estar se perguntando como funciona a especificidade do CSS se o elemento tiver estilos definidos no Shadow DOM. Em termos de especificidade, os estilos do usuário predominam. Eles sempre modificam o estilo definido no elemento. Consulte a seção sobre Como criar um elemento que usa o Shadow DOM.

Aplicar estilo predefinido em elementos não registrados

Antes que um elemento seja atualizado, você pode fazer referência a ele no CSS usando a pseudoclasse :defined. Isso é útil para aplicar estilo predefinido a um componente. Por exemplo, para evitar layout ou outro flash de conteúdo sem estilo (FOUC) visual, oculte componentes indefinidos e exiba-os quando forem definidos.

Exemplo: oculte <app-drawer> antes de definir:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

Depois que <app-drawer> for definido, o seletor (app-drawer:not(:defined)) não vai mais corresponder.

Como estender elementos

A API Custom Elements é útil para criar novos elementos HTML, mas também pode ampliar outros elementos personalizados ou até mesmo o HTML integrado no navegador.

Como estender um elemento personalizado

A extensão de outro elemento personalizado é feita estendendo a definição da classe.

Exemplo: crie <fancy-app-drawer> que estende <app-drawer>:

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

Como estender elementos HTML nativos

Vamos supor que você queira criar uma <button> mais sofisticada. Em vez de replicar o comportamento e a funcionalidade de <button>, uma opção melhor é aprimorar progressivamente o elemento usando elementos personalizados.

Um elemento incorporado personalizado é um elemento personalizado que estende uma das tags HTML incorporadas do navegador. A principal vantagem da extensão de um elemento existente é ganhar todos os recursos dele (propriedades do DOM, métodos, acessibilidade). Não há uma forma melhor de criar um app da Web progressivo do que aprimorar progressivamente elementos HTML existentes.

Para estender um elemento, será necessário criar uma definição de classe que herde da interface DOM correta. Por exemplo, um elemento personalizado que estende <button> precisa herdar de HTMLButtonElement em vez de HTMLElement. Da mesma forma, um elemento que estende <img> precisa estender HTMLImageElement.

Exemplo: como estender <button>:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

Observe que a chamada a define() muda um pouco ao estender um elemento nativo. O terceiro parâmetro, obrigatório, informa ao navegador qual tag está sendo estendida. Isso é necessário porque muitas tags HTML compartilham a mesma interface DOM. <section>, <address> e <em> (entre outros) compartilham HTMLElement; <q> e <blockquote> compartilham HTMLQuoteElement; etc. Especificar {extends: 'blockquote'} informa ao navegador que você está criando uma <blockquote> melhorada em vez de uma <q>. Consulte a especificação do HTML para conferir a lista completa de interfaces do DOM do HTML.

Os clientes de um elemento personalizado incorporado podem usá-lo de diversas formas. Eles podem declará-la adicionando o atributo is="" à tag nativa:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

Crie uma instância no JavaScript:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

ou use o operador new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

Confira outro exemplo que estende <img>.

Exemplo: como estender <img>:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

Os usuários declaram esse componente como:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

ou criam uma instância no JavaScript:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

Detalhes diversos

Elementos desconhecidos x elementos personalizados indefinidos

O HTML pode ser usado de forma leniente e flexível. Por exemplo, declare <randomtagthatdoesntexist> em uma página e o navegador vai aceitar isso sem problemas. Por que as tags não padrão funcionam? A resposta é que a especificação do HTML permite isso. Elementos não definidos na especificação são analisados como HTMLUnknownElement.

O mesmo não acontece com elementos personalizados. Os possíveis elementos personalizados são analisados como HTMLElement se criados com um nome válido (incluindo um "-"). Você pode verificar isso em um navegador que ofereça suporte a elementos personalizados. Ative o console: Ctrl+Shift+J (ou Cmd+Opt+J no Mac) e cole as seguintes linhas de código:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

Referência da API

O customElements global define métodos úteis para trabalhar com elementos personalizados.

define(tagName, constructor, options)

Define um novo elemento personalizado no navegador.

Exemplo

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

Dado um nome de tag de elemento personalizado válido, retorna o construtor do elemento. Retorna undefined se nenhuma definição de elemento foi registrada.

Exemplo

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

Retorna uma promessa que será resolvida quando o elemento personalizado for definido. Se o elemento já estiver definido, resolva imediatamente. Rejeita se o nome da tag não for um nome de elemento personalizado válido.

Exemplo

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

Histórico e suporte a navegadores

Se você acompanhou os componentes da Web nos últimos dois anos, sabe que o Chrome 36+ implementou uma versão da API Custom Elements que usa document.registerElement() em vez de customElements.define(). Agora, ela é considerada uma versão descontinuada do padrão, chamada v0. customElements.define() é a nova tendência e o que os fornecedores de navegadores estão começando a implementar. É a versão Custom Elements v1.

Se você ainda estiver interessado na especificação v0 antiga, confira o artigo de html5rocks.

Suporte ao navegador

O Chrome 54 (status), o Safari 10.1 (status) e o Firefox 63 (status) têm a versão 1 dos elementos personalizados. O Edge iniciou o desenvolvimento.

Para detectar elementos personalizados, verifique a existência de window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

Até que o suporte do navegador esteja amplamente disponível, há um polyfill autônomo disponível para a versão 1 dos Elementos personalizados. No entanto, recomendamos usar o webcomponents.js loader para carregar da melhor forma possível os polyfills de componentes da Web. O loader usa a detecção de recursos para carregar de forma assíncrona apenas os pollyfills necessários requeridos pelo navegador.

Instale:

npm install --save @webcomponents/webcomponentsjs

Uso:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

Conclusão

Os elementos personalizados nos oferecem uma nova ferramenta para definir novas tags HTML no navegador e criar componentes reutilizáveis. Combine-os com outros novos primitivos da plataforma, como Shadow DOM e <template>, para começar a perceber a abrangência geral dos Web Components:

  • Compatível com todos os navegadores (padrão da Web) para criar e estender componentes reutilizáveis.
  • Não exige biblioteca ou framework para começar a usar. JS/HTML comum... fantástico!
  • Oferece um modelo de programação conhecido. É apenas DOM/CSS/HTML.
  • Funciona bem com outros novos recursos da plataforma da Web (Shadow DOM, <template>, propriedades personalizadas do CSS etc.)
  • Estreitamente integrado ao DevTools do navegador.
  • Use os recursos de acessibilidade atuais.