Elementos personalizados v1 - Componentes da Web reutilizáveis

Com os elementos personalizados, os desenvolvedores da Web podem definir novas tags HTML, estender as existentes e criar 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 traz uma maneira baseada em padrões da Web de criar componentes reutilizáveis usando nada mais do que JS/HTML/CSS básico. O resultado é menos código, código modular e mais reutilização nos apps.

Introdução

O navegador é uma excelente ferramenta para estruturar aplicativos da Web. Chama-se HTML. Talvez você já tenha ouvido falar disso. É declarativo, portátil, com suporte e fácil de trabalhar. Por melhor que seja o HTML, seu vocabulário e extensibilidade são limitados. Até agora, o padrão atual do HTML (link em inglês) não tinha uma maneira de associar automaticamente o comportamento do JavaScript à marcação.

Os elementos personalizados são a resposta à modernização do HTML, preenchendo as partes que faltam e agrupando a estrutura com o comportamento. Se o HTML não fornecer a solução para um problema, poderemos criar um elemento personalizado que faça isso. Os elementos personalizados ensinam novos truques ao navegador, preservando os benefícios do HTML.

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 estenda a HTMLElement base.

Exemplo: definindo 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 usar um elemento personalizado não é diferente de usar um <div> ou qualquer outro elemento. As instâncias podem ser declaradas na página, criadas dinamicamente em JavaScript, listeners de eventos podem ser anexados etc. Continue lendo para saber mais.

Definir a API JavaScript de um elemento

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

Exemplo: definindo a 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, estamos criando uma gaveta que tem uma propriedade open, uma propriedade disabled e um método toggleDrawer(). Ele também reflete as propriedades como atributos HTML.

Uma característica interessante dos elementos personalizados é que this dentro de uma definição de classe se refere ao próprio elemento DOM, ou seja, a instância da classe. No nosso exemplo, this refere-se a <app-drawer>. É assim que o elemento pode anexar um listener click a si mesmo (isso). E você não está limitado a listeners de eventos. Toda a API do DOM 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 a criação de elementos personalizados

  1. O nome de um elemento personalizado precisa conter um traço (-). Portanto, <x-tags>, <my-element> e <my-awesome-app> são nomes válidos, ao contrário de <tabs> e <foo_bar>. Esse requisito serve para que o analisador HTML possa distinguir elementos personalizados de elementos regulares. Ele também garante compatibilidade direta quando novas tags são adicionadas ao HTML.
  2. Não é possível registrar a mesma tag mais de uma vez. Uma tentativa de fazer isso vai gerar uma DOMException. Após informar o navegador sobre uma nova tag, é só. Sem devolução.
  3. Os elementos personalizados não podem fechar automaticamente porque o HTML permite que apenas alguns elementos se fechem automaticamente. Sempre escreva uma tag de fechamento (<app-drawer></app-drawer>).

Reações com elementos personalizados

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

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

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

Exemplo:adicionar reações de elemento personalizado ao <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 suficientemente complexo e abrir uma conexão com o IndexedDB em connectedCallback(), faça o trabalho de limpeza necessário em disconnectedCallback(). Mas tome cuidado. Não é possível confiar que o elemento seja 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 para atributos

É comum que as propriedades HTML reflitam o valor delas no DOM como um atributo HTML. Por exemplo, quando os valores de hidden ou id são alterados no 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 refletir propriedades para atributos. Quase todas as propriedades em HTML fazem isso. Por quê? Os atributos também são úteis para configurar um elemento de maneira declarativa, e algumas APIs, como acessibilidade e seletores CSS, dependem desses atributos para funcionar.

A reflexão de uma propriedade é útil sempre que você quer manter a representação do DOM do elemento sincronizada com o estado do JavaScript. Um motivo para refletir uma propriedade é aplicar o estilo definido pelo usuário quando o estado do JS mudar.

Lembre-se do nosso <app-drawer>. Um consumidor desse componente pode querer esmaecer e/ou evitar a interação do usuário quando ele estiver desativado:

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

Quando a propriedade disabled é modificada no JavaScript, 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 em atributos

Os atributos HTML são uma maneira conveniente para os usuários declararem o estado inicial:

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

Os elementos podem reagir a mudanças de atributos definindo um attributeChangedCallback. O navegador 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, vamos definir atributos adicionais no <app-drawer> quando um atributo disabled é modificado. Embora não estejamos fazendo isso aqui, também é possível usar 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ê precisa definir e registrar um elemento personalizado de uma só vez.

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

O aprimoramento progressivo é um recurso dos elementos personalizados. Em outras palavras, é possível declarar vários elementos <app-drawer> na página e só invocar customElements.define('app-drawer', ...) muito mais tarde. Isso ocorre porque o navegador trata possíveis elementos personalizados de maneira diferente graças a tags desconhecidas. O processo de chamar define() e aprimorar 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(). Ela 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 por 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 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

Como criar um elemento que usa o Shadow DOM

O Shadow DOM permite que um elemento tenha, renderize e estilize um bloco do DOM separado do restante da página. Você pode até ocultar um aplicativo 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 de 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 uma <template>

Para quem não conhece, o elemento <template> permite declarar fragmentos do DOM que são analisados, ficam inertes no carregamento da página e podem ser ativados posteriormente no momento da 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 os principais motivos:

  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 definido para o elemento, graças ao Shadow DOM

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

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

Definir o estilo de um elemento personalizado

Mesmo que seu elemento defina um estilo próprio usando o Shadow DOM, os usuários poderão definir o estilo do elemento personalizado na página deles. 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>

Talvez você esteja 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 vencem. Eles sempre vão substituir o estilo definido pelo elemento. Consulte a seção Como criar um elemento que usa o Shadow DOM.

Como definir previamente o estilo de elementos não registrados

Antes de fazer o upgrade de um elemento, é possível segmentá-lo no CSS usando a pseudoclasse :defined. Isso é útil para pré-estilizar um componente. Por exemplo, para evitar o layout ou outro FOUC visual, oculte componentes indefinidos e exiba-os quando forem definidos.

Exemplo: oculte <app-drawer> antes de ser 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, além de ampliar outros elementos personalizados ou até mesmo o HTML integrado do navegador.

Estender um elemento personalizado

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

Exemplo - criar <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);

Estender elementos HTML nativos

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

Um elemento personalizado integrado é aquele que estende uma das tags HTML integradas do navegador. O principal benefício da extensão de um elemento existente é conseguir todos os seus recursos (propriedades do DOM, métodos, acessibilidade). A maneira melhor de criar um Progressive Web App é aprimorar progressivamente elementos HTML existentes.

Para estender um elemento, você precisa criar uma definição de classe herdada 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: estendendo <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 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'} permite que o navegador saiba que você está criando um <blockquote> atualizado em vez de um <q>. Consulte a especificação HTML para ver a lista completa de interfaces DOM do HTML.

Os consumidores de um elemento integrado personalizado podem usá-lo de várias maneiras. Para declará-lo, adicione 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: estendendo <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 pode ser usado. Por exemplo, declare <randomtagthatdoesntexist> em uma página e o navegador aceitará a ação. Por que as tags não padrão funcionam? A resposta é a especificação HTML (em inglês) que permite isso. Os elementos que não são definidos pela especificação são analisados como HTMLUnknownElement.

O mesmo não acontece com elementos personalizados. Os possíveis elementos personalizados serão analisados como uma HTMLElement se forem criados com um nome válido (incluindo um "-"). É possível verificar isso em um navegador compatível com elementos personalizados. Abra 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. Será rejeitada 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 do navegador

Se você tem seguido componentes da Web nos últimos 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 popularidade e o que os fornecedores de navegador estão começando a implementar. É a versão Custom Elements v1.

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

Suporte ao navegador

O Chrome 54 (status), o Safari 10.1 (status) e o Firefox 63 (status) têm elementos personalizados v1. O Edge começou o desenvolvimento.

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

const supportsCustomElementsV1 = 'customElements' in window;

Plástico poligonal

Até que a compatibilidade com o navegador esteja amplamente disponível, há um polyfill independente disponível para os elementos personalizados v1. No entanto, recomendamos usar o carregador da webcomponents.js (em inglês) para carregar os polyfills de componentes da Web de maneira otimizada. O carregador usa a detecção de recursos para carregar de forma assíncrona apenas os polyfills necessários exigidos pelo navegador.

Instale-o:

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 outros novos primitivos da plataforma, como o Shadow DOM e o <template>, para começar a perceber a visão geral dos componentes da Web:

  • Vários navegadores (padrão da Web) para criar e estender componentes reutilizáveis.
  • Não exige biblioteca ou framework para começar. JS/HTML baunilha FTW!
  • fornece 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 de CSS etc.).
  • Estreitamente integrado ao DevTools do navegador.
  • Aproveite os recursos de acessibilidade atuais.