Elementos personalizados v1 - Componentes da Web reutilizáveis

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

Com os elementos personalizados, os desenvolvedores da Web podem criar novas tags HTML, melhorar as tags HTML existentes ou estender os componentes criados por outros desenvolvedores. A API é a base dos componentes da Web. Ele traz uma maneira baseada em padrões da Web para criar componentes reutilizáveis usando apenas JS/HTML/CSS vanilla. O resultado é menos código, código modular e mais reutilização nos nossos apps.

Introdução

O navegador é uma ferramenta excelente para estruturar aplicativos da Web. Ele se chama HTML. Você já deve ter ouvido falar sobre isso. Ele é declarativo, portátil, tem suporte bom e é fácil de usar. Por mais que o HTML seja ótimo, o vocabulário e a extensibilidade dele são limitados. O padrão ativo do HTML sempre carecia de uma maneira de associar automaticamente o comportamento do JS à sua marcação... até agora.

Os elementos personalizados são a resposta para modernizar o HTML, preencher as partes ausentes e agrupar a estrutura com o comportamento. Se o HTML não fornecer a solução para um problema, podemos criar um elemento personalizado que o faça. Os elementos personalizados ensinam novos truques ao navegador, preservando os benefícios do HTML.

Como definir um novo elemento

Para definir um novo elemento HTML, precisamos do poder do JavaScript.

O customElements global é 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 JavaScript que estende o HTMLElement 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 de qualquer outro elemento. As instâncias podem ser declaradas na página, criadas dinamicamente em JavaScript, os listeners de eventos podem ser anexados 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 com uma propriedade open, uma propriedade disabled e um método toggleDrawer(). Ele também reflete propriedades como atributos HTML.

Um recurso interessante 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 (😉) o elemento pode anexar um listener click a si mesmo. E você não está limitado a listeners de eventos. A API DOM inteira está disponível no 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 criar 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. Esse requisito é para que o analisador HTML possa distinguir elementos personalizados de elementos normais. Ele também garante a compatibilidade futura quando novas tags são adicionadas ao HTML.
  2. Não é possível registrar a mesma tag mais de uma vez. Tentar fazer isso gerará um DOMException. Depois de informar ao navegador sobre uma nova tag, é isso. Não é possível cancelar.
  3. Os elementos personalizados não podem ser auto-fechamento porque o HTML permite apenas alguns elementos auto-fechamento. 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 executar o código durante momentos interessantes da existência dele. Elas são chamadas de reações de elementos personalizados.

Nome Chamado quando
constructor Uma instância do elemento é criada ou upgradeada. Útil para inicializar o estado, configurar listeners de eventos ou criar um DOM sombra. Consulte a especificação para ver as restrições do que é possível fazer no constructor.
connectedCallback É chamado toda vez que o elemento é inserido no DOM. Útil para executar o código de configuração, como buscar recursos ou renderizar. Em geral, tente adiar o trabalho até esse horário.
disconnectedCallback É chamado toda vez que o elemento é removido do DOM. Útil para executar o 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 seu 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, quando o usuário chama el.remove()).

Exemplo:adição de reações de elementos personalizadas 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 fizer sentido. Se o elemento for suficientemente complexo e abrir uma conexão com o IndexedDB em connectedCallback(), faça a limpeza necessária em disconnectedCallback(). Mas tenha cuidado. Não é possível confiar que seu elemento será 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 HTML reflitam o valor de volta para o DOM como um atributo 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 tempo real como atributos:

<div id="my-id" hidden>

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

Refletir uma propriedade é útil em qualquer lugar em que você queira manter a representação do DOM do elemento em sincronia com o estado do JavaScript. Uma das razões para refletir uma propriedade é para que o estilo definido pelo usuário seja aplicado quando o estado do JS mudar.

Lembre-se de <app-drawer>. Um consumidor desse componente pode querer desativá-lo e/ou impedir a interação do usuário quando ele 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 com o 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 maneira conveniente de declarar o estado inicial:

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

Os elementos podem reagir a 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 outros atributos no <app-drawer> quando um atributo disabled é alterado. Embora não estejamos fazendo isso aqui, você também pode usar o attributeChangedCallback para manter uma propriedade 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ê precisa definir e registrar um elemento personalizado de uma só vez.

Os elementos personalizados podem ser usados antes de a definição deles ser registrada.

O aprimoramento progressivo é um recurso de elementos personalizados. Em outras palavras, é possível declarar vários elementos <app-drawer> na página e nunca invocar customElements.define('app-drawer', ...) até muito mais tarde. Isso ocorre porque o navegador trata os elementos personalizados em potencial de maneira diferente devido às tags desconhecidas. O processo de chamar define() e atribuir um elemento existente com uma definição de classe é chamado de "upgrades de elemento".

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 no código do elemento. As reações são úteis para isso.

Exemplo: crie um elemento com um 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 produzir:

<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 oferece uma maneira de um elemento possuir, renderizar e estilizar um pedaço de DOM separado do restante da página. Você pode até ocultar um app inteiro 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 com um <template>

Para quem não conhece, o elemento <template> permite declarar fragmentos do DOM que são analisados, inertes no carregamento da página e podem ser ativados mais tarde no tempo de execução. É outra primitiva de API na família de componentes da Web. Os modelos são um marcador de posição ideal para declarar a estrutura de um elemento personalizado.

Exemplo:registrar um elemento com conteúdo do shadow DOM criado a partir de 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 são muito eficientes. Vamos entender os principais pontos em andamento:

  1. Estamos definindo um novo elemento em HTML: <x-foo-from-template>
  2. O shadow DOM do elemento é criado a partir de um <template>
  3. O DOM do elemento é local para o elemento graças ao shadow DOM
  4. O CSS interno do elemento é limitado ao elemento graças ao shadow DOM

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

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

Como definir o estilo de um elemento personalizado

Mesmo que seu elemento defina o 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 a especificidade do CSS funciona se o elemento tiver estilos definidos no Shadow DOM. Em termos de especificidade, os estilos do usuário são os vencedores. Eles sempre vão substituir o estilo definido pelo elemento. Consulte a seção Criar um elemento que usa o shadow DOM.

Estilo prévio de elementos não registrados

Antes que um elemento seja atualizado, é possível segmentá-lo no CSS usando a pseudoclasse :defined. Isso é útil para pré-estilizar um componente. Por exemplo, você pode impedir o layout ou outro FOUC visual ocultando componentes indefinidos e os mostrando quando eles forem definidos.

Exemplo: oculte <app-drawer> antes que ele seja definido:

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> é definido, o seletor (app-drawer:not(:defined)) não corresponde mais.

Como estender elementos

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

Como estender um elemento personalizado

Para estender outro elemento personalizado, estenda 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

Digamos 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 integrado personalizado é um elemento personalizado que estende uma das tags HTML integradas do navegador. O principal benefício de estender um elemento já existente é ter acesso a todos os recursos dele (propriedades DOM, métodos, acessibilidade). Não há uma maneira melhor de escrever um app Web progressivo do que melhorar progressivamente os elementos HTML atuais.

Para estender um elemento, você precisa criar uma definição de classe que herda 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'});

A chamada para define() muda um pouco ao estender um elemento nativo. O terceiro parâmetro obrigatório informa ao navegador qual tag você está estendendo. 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> aprimorada em vez de uma <q>. Consulte a especificação do HTML para conferir a lista completa de interfaces DOM do HTML.

Os consumidores de um elemento integrado personalizado podem usá-lo de várias maneiras. Eles podem declarar isso adicionando o atributo is="" à tag nativa:

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

Crie uma instância em 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 crie uma instância em 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 é flexível e fácil de trabalhar. Por exemplo, declare <randomtagthatdoesntexist> em uma página, e o navegador a aceita sem problemas. Por que as tags não padronizadas funcionam? A resposta é que a especificação HTML permite isso. Os elementos que não são definidos pela especificação são analisados como HTMLUnknownElement.

Isso não é verdade para elementos personalizados. Elementos personalizados em potencial são analisados como HTMLElement se forem criados com um nome válido (que inclua 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 tiver sido registrada.

Exemplo

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

whenDefined(tagName)

Retorna uma promessa que é resolvida quando o elemento personalizado é 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ê acompanha os componentes da Web há alguns anos, sabe que o Chrome 36 e versões mais recentes implementaram uma versão da API Custom Elements que usa document.registerElement() em vez de customElements.define(). Ela agora é 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. Ela se chama Elementos personalizados v1.

Se você tiver interesse na especificação v0 antiga, confira o artigo 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 já começou a ser desenvolvido.

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 Elementos personalizados v1. 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 oferecem uma nova ferramenta para definir novas tags HTML no navegador e criar componentes reutilizáveis. Combine-os com as outras primitivas de plataforma novas, como o Shadow DOM e o <template>, e você vai começar a entender o panorama geral dos componentes da Web:

  • Entre navegadores (padrão da Web) para criar e estender componentes reutilizáveis.
  • Não requer biblioteca ou framework para começar. Vanilla JS/HTML FTW!
  • Oferece um modelo de programação conhecido. É apenas DOM/CSS/HTML.
  • Funciona bem com outros novos recursos da plataforma da Web (DOM de sombra, <template>, propriedades personalizadas de CSS etc.)
  • Integração total com as ferramentas do desenvolvedor do navegador.
  • Aproveite os recursos de acessibilidade atuais.