Elementos personalizados v1: Componentes web reutilizables

Los elementos personalizados permiten a los desarrolladores web definir nuevas etiquetas HTML, extender las existentes y crear componentes web reutilizables.

Con los elementos personalizados, los desarrolladores web pueden crear etiquetas HTML nuevas, reforzar las etiquetas HTML existentes o extender los componentes que crearon otros desarrolladores. La API es la base de los componentes web. Aporta una estrategia web basada en estándares para crear componentes reutilizables usando solo JS/HTML/CSS básicos. Como resultado, se obtiene una reducción del código, la modularización de este y una mayor capacidad de reutilización en nuestras apps.

Introducción

El navegador nos proporciona una herramienta excelente para estructurar aplicaciones web. Se llama HTML. ¡Es posible que hayas oído hablar de ella! Es declarativo, portátil, ampliamente compatible y fácil de usar. Si bien el lenguaje HTML es excelente, su vocabulario y extensibilidad son limitados. El estándar HTML siempre careció de una forma de asociar automáticamente el comportamiento de JS con tu lenguaje de marcado… hasta ahora.

Los elementos personalizados son la respuesta a la modernización de HTML, completan las piezas faltantes y agrupan estructura y comportamiento. Si HTML no proporciona la solución a un problema, podemos crear un elemento personalizado que lo haga. Los elementos personalizados transmiten nuevos trucos al navegador y conservan los beneficios del HTML.

Cómo definir un elemento nuevo

Para definir un nuevo elemento HTML, necesitamos el poder de JavaScript.

El elemento customElements global se usa para definir un elemento personalizado y notificar al navegador sobre una nueva etiqueta. Llama a customElements.define() con el nombre de etiqueta que deseas crear y una class de JavaScript que extienda la HTMLElement base.

Ejemplo: Definición de un panel lateral para dispositivos móviles, <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 {...});

Ejemplo de uso:

<app-drawer></app-drawer>

Es importante recordar que el uso de un elemento personalizado no es diferente del uso de un <div> o cualquier otro elemento. Las instancias se pueden declarar en la página, crear de forma dinámica en JavaScript, adjuntar objetos de escucha de eventos, etcétera. Sigue leyendo para ver más ejemplos.

Cómo definir la API de JavaScript de un elemento

La funcionalidad de un elemento personalizado se define con un class de ES2015 que extiende HTMLElement. La extensión de HTMLElement garantiza que el elemento personalizado herede toda la API de DOM y significa que cualquier propiedad o método que agregues a la clase se convierta en parte de la interfaz de DOM del elemento. En esencia, usa la clase para crear una API de JavaScript pública para tu etiqueta.

Ejemplo: Definición de la interfaz de 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);

En este ejemplo, se crea un panel lateral con una propiedad open, una propiedad disabled y un método toggleDrawer(). También refleja propiedades como atributos HTML.

Una excelente función de los elementos personalizados es que this, dentro de una definición de clase, hace referencia al elemento del DOM, es decir, la instancia de la clase. En nuestro ejemplo, this hace referencia a <app-drawer>. De esta manera (😉) es cómo el elemento puede adjuntarse un objeto de escucha click a sí mismo. Además, no estarás limitado a los objetos de escucha de eventos. La API de DOM completa se encuentra disponible en el código del elemento. Usa this para acceder a las propiedades del elemento, inspeccionar sus campos secundarios (this.children), consultar nodos (this.querySelectorAll('.items')), entre otras posibilidades.

Reglas para la creación de elementos personalizados

  1. El nombre de un elemento personalizado debe contener un guion (-). Por lo tanto, <x-tags>, <my-element> y <my-awesome-app> son nombres válidos, mientras que <tabs> y <foo_bar> no lo son. Este requisito está pensado para que el analizador HTML pueda distinguir los elementos personalizados de los comunes. También garantiza la compatibilidad con versiones futuras cuando se agreguen etiquetas nuevas a HTML.
  2. No puedes registrar la misma etiqueta más de una vez. Si intentas hacerlo, se arrojará una DOMException. Una vez que notifiques al navegador sobre la nueva etiqueta, el trabajo estará hecho. No hay devoluciones.
  3. Los elementos personalizados no se pueden cerrar automáticamente, ya que HTML solo permite que unos pocos elementos se cierren por sí solos. Escribe siempre una etiqueta de cierre (<app-drawer></app-drawer>).

Reacciones de elementos personalizados

Un elemento personalizado puede definir hooks de ciclo de vida especiales para ejecutar código durante momentos interesantes de su existencia. Estas se llaman reacciones de elementos personalizados.

Nombre Se llama cuando
constructor Se crea o actualiza una instancia del elemento. Es útil para inicializar el estado, configurar objetos de escucha de eventos o crear un Shadow DOM. Consulta la especificación para conocer las restricciones en relación con lo que puedes hacer en constructor.
connectedCallback Se llama cada vez que se inserta el elemento en el DOM. Es útil para ejecutar código de configuración, como la recuperación de recursos o la renderización. En general, debes intentar demorar el trabajo hasta este momento.
disconnectedCallback Se llama cada vez que se quita el elemento del DOM. Es útil para ejecutar código de limpieza.
attributeChangedCallback(attrName, oldVal, newVal) Se llama cuando se agrega, quita, actualiza o reemplaza un atributo observado. También se llama para obtener valores iniciales cuando el analizador crea un elemento o lo actualiza. Nota: Solo los atributos que se indican en la propiedad observedAttributes recibirán esta devolución de llamada.
adoptedCallback El elemento personalizado se trasladó a un nuevo document (p. ej., alguien llamó a document.adoptNode(el)).

Las devoluciones de llamada de reacción son síncronas. Si alguien llama a el.setAttribute() en tu elemento, el navegador llamará de inmediato a attributeChangedCallback(). De manera similar, recibirás un disconnectedCallback() después de que se quite tu elemento del DOM (p. ej., el usuario llama a el.remove()).

Ejemplo: Se agregaron reacciones de elementos personalizados a <app-drawer>:

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

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

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

Define reacciones cuando tengan sentido. Si tu elemento es lo suficientemente complejo y abre una conexión con IndexedDB en connectedCallback(), realiza las tareas de limpieza necesarias en disconnectedCallback(). Pero ten cuidado. No puedes confiar en que tu elemento se quite del DOM en todas las circunstancias. Por ejemplo, nunca se llamará a disconnectedCallback() si el usuario cierra la pestaña.

Propiedades y atributos

Cómo reflejar propiedades en atributos

Es común que las propiedades HTML reflejen su valor en el DOM como un atributo HTML. Por ejemplo, cuando se cambian los valores de hidden o id en JS:

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

los valores se aplican al DOM activo como atributos:

<div id="my-id" hidden>

Esto se denomina "cómo reflejar propiedades en los atributos". Casi todas las propiedades en HTML hacen esto. ¿Por qué? Los atributos también son útiles para configurar un elemento de forma declarativa, y el funcionamiento de ciertas APIs, como los selectores de CSS y accesibilidad, depende de los atributos.

Reflejar una propiedad es útil cuando deseas mantener la representación del DOM del elemento en sincronización con su estado de JavaScript. Un motivo por el cual podría convenirte reflejar una propiedad es la aplicación de los estilos definidos por el usuario cuando cambie el estado de JS.

Recuerda nuestro <app-drawer>. Un consumidor de este componente podría deseas que se desvanezca o evitar la interacción del usuario cuando esté inhabilitado:

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

Cuando se modifica la propiedad disabled en JS, se busca agregar ese atributo al DOM de modo que coincida con el selector del usuario. El elemento puede proporcionar ese comportamiento si refleja el valor en un atributo con el mismo nombre:

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();
}

Observa los cambios en los atributos

Los atributos HTML permiten a los usuarios declarar el estado inicial de una manera conveniente:

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

Los elementos pueden reaccionar a los cambios de atributo definiendo un attributeChangedCallback. El navegador llamará a este método para cada cambio en los atributos que se indiquen en el array 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.
  }
}

En el ejemplo, se configuran atributos adicionales en el <app-drawer> cuando se cambia un atributo disabled. Si bien no lo haremos aquí, también podrías usar attributeChangedCallback para mantener una propiedad JS sincronizada con su atributo.

Actualizaciones de elementos

HTML progresivamente mejorado

Ya vimos que los elementos personalizados se definen llamando a customElements.define(). Pero esto no significa que debes definir y registrar un elemento personalizado en una sola operación.

Los elementos personalizados se pueden usar antes de registrar su definición.

La mejora progresiva es una característica de los elementos personalizados. En otras palabras, puedes declarar un grupo de elementos <app-drawer> en la página y no invocar a customElements.define('app-drawer', ...) hasta mucho más adelante. Esto se debe a que el navegador da a los posibles elementos personalizados un tratamiento diferente al de las etiquetas desconocidas. El proceso de llamar a define() y extender a un elemento existente con una definición de clase se denomina "actualizaciones de elementos".

Para saber cuándo se define el nombre de una etiqueta, puedes usar window.customElements.whenDefined(). Muestra una promesa que se resuelve cuando se define el elemento.

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

Ejemplo: Retrasa el trabajo hasta que se actualice un conjunto de elementos secundarios

<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.
});

Contenido definido por elementos

Los elementos personalizados pueden administrar su propio contenido usando las APIs de DOM en el código del elemento. Las reacciones son útiles para esto.

Ejemplo: Crea un elemento con HTML predeterminado:

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

La declaración de esta etiqueta producirá lo siguiente:

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

// TODO: DevSite: Se quitó el ejemplo de código porque usaba controladores de eventos intercalados

Cómo crear un elemento que use Shadow DOM

Shadow DOM proporciona una alternativa para que un elemento posea una parte del DOM independiente del resto de la página, la represente y le aplique estilo. Podrías, incluso, ocultar una app completa en una sola etiqueta:

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

Para usar Shadow DOM en un elemento personalizado, llama a this.attachShadow dentro de tu 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));
  }
  // ...
});

Ejemplo 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 del usuario

// TODO: DevSite - Se quitó la muestra de código porque usaba controladores de eventos intercalados

Cómo crear elementos a partir de un <template>

Para quienes no lo conozcan, el elemento <template> te permite declarar fragmentos del DOM que se analizan, permanecen inactivos durante la carga de la página y se pueden activar más adelante en el tiempo de ejecución. Es otra primitiva de API de la familia de componentes web. Las plantillas son marcadores de posición ideales para declarar la estructura de un elemento personalizado.

Ejemplo: Cómo registrar un elemento con contenido de Shadow DOM creado a partir de un <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>

Estas pocas líneas de código tienen una gran capacidad. Veamos los puntos clave:

  1. Definiremos un nuevo elemento en HTML: <x-foo-from-template>
  2. El Shadow DOM del elemento se crea a partir de un <template>
  3. El DOM del elemento es local del elemento gracias al Shadow DOM.
  4. La CSS interna del elemento se aplica a este gracias a Shadow DOM.

I'm in Shadow DOM. Mi marcado se selló a partir de una <template>.

// TODO: DevSite: Se quitó el ejemplo de código porque usaba controladores de eventos intercalados

Cómo aplicar estilo a un elemento personalizado

Incluso si tu elemento define su propio estilo usando Shadow DOM, los usuarios pueden aplicar estilo a tu elemento personalizado desde sus páginas. Estos se denominan “estilos definidos por el usuario”.

<!-- 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>

Quizá te preguntes cómo funciona la especificidad de la CSS si el elemento tiene estilos definidos dentro de Shadow DOM. En términos de especificidad, prevalecen los estilos del usuario. Siempre anularán el diseño definido por el elemento. Consulta la sección sobre cómo crear un elemento que use Shadow DOM.

Cómo aplicar diseño previo a elementos no registrados

Antes de que un elemento se actualice, puedes apuntar a él en la CSS con la seudoclase :defined. Esto es útil para aplicar estilo previo a un componente. Por ejemplo, quizá desees evitar el diseño u otro FOUC visual ocultando los componentes sin definir y aplicándoles difuminación de entrada cuando se vuelvan definidos.

Ejemplo: oculta <app-drawer> antes de que se defina:

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;
}

Una vez que se define <app-drawer>, el selector (app-drawer:not(:defined)) deja de coincidir.

Cómo extender elementos

La Custom Elements API es útil para crear nuevos elementos HTML, pero también es útil para extender otros elementos personalizados o incluso el HTML integrado del navegador.

Cómo extender un elemento personalizado

La extensión de otro elemento personalizado se realiza mediante la extensión de su definición de clase.

Ejemplo: Crea <fancy-app-drawer> que extienda <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);

Cómo extender elementos HTML nativos

Supongamos que quieres crear un <button> más atractivo. En lugar de replicar el comportamiento y la funcionalidad de <button>, una mejor opción es mejorar de forma progresiva el elemento existente usando elementos personalizados.

Un elemento integrado personalizado es un elemento personalizado que extiende una de las etiquetas HTML integradas del navegador. El principal beneficio de extender un elemento existente es contar con todas sus características (propiedades del DOM, métodos y accesibilidad). La mejor manera de escribir una app web progresiva es mejorar progresivamente los elementos HTML existentes.

Para extender un elemento, deberás crear una definición de clase que herede de la interfaz correcta del DOM. Por ejemplo, un elemento personalizado que extiende <button> debe heredar de HTMLButtonElement en lugar de HTMLElement. De manera similar, un elemento que extiende <img> debe extender HTMLImageElement.

Ejemplo, extendiendo <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'});

Observa que la llamada a define() cambia ligeramente cuando se extiende un elemento nativo. El tercer parámetro obligatorio indica al navegador la etiqueta que extenderás. Esto es necesario porque muchas etiquetas HTML comparten la misma interfaz del DOM. <section>, <address> y <em> (entre otros) comparten HTMLElement; <q> y <blockquote> comparten HTMLQuoteElement, etcétera. Especificar {extends: 'blockquote'} le permite al navegador saber que estás creando un <blockquote> mejorado en lugar de un <q>. Consulta la especificación de HTML para obtener la lista completa de interfaces de DOM del HTML.

Los consumidores de un elemento integrado personalizado pueden usarlo de varias maneras. Para declararlo, agrega el atributo is="" en la etiqueta nativa:

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

Crea una instancia en 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);

o usa el operador new:

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

Este es otro ejemplo que extiende <img>.

Ejemplo: Extensión de <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'});

Los usuarios declaran este componente de la siguiente manera:

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

También puedes crear una instancia en 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);

Detalles varios

Elementos desconocidos frente a elementos personalizados sin definir

El lenguaje HTML es flexible y flexible para trabajar. Por ejemplo, declara <randomtagthatdoesntexist> en una página y el navegador lo aceptará sin problemas. ¿Por qué funcionan las etiquetas no estándares? La respuesta es que la especificación de HTML lo permite. Los elementos que la especificación no define se analizan como HTMLUnknownElement.

No ocurre lo mismo con los elementos personalizados. Los posibles elementos personalizados se analizan como un HTMLElement si se crean con un nombre válido (se incluye “-”). Puedes comprobar esto en un navegador que admita elementos personalizados. Inicia la consola: Ctrl+Mayúsculas+J (o Cmd+Opt+J en Mac) y pega las siguientes líneas 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

Referencia de la API

El elemento global customElements define métodos útiles para trabajar con elementos personalizados.

define(tagName, constructor, options)

Define un nuevo elemento personalizado en el navegador.

Ejemplo

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

get(tagName)

Cuando se proporciona un nombre de etiqueta válido a un elemento personalizado, se muestra el constructor del elemento. Muestra undefined si no se registró una definición para el elemento.

Ejemplo

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

whenDefined(tagName)

Muestra una Promesa que se resuelve cuando se define el elemento personalizado. Si el elemento ya está definido, resuélvelo de inmediato. Se rechaza si el nombre de la etiqueta no es un nombre de elemento personalizado válido.

Ejemplo

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

Historial y compatibilidad del navegador

Si has estado al tanto de los componentes web durante los últimos años, sabrás que en Chrome 36 (y versiones posteriores) se implementó una versión de la Custom Elements API en la cual se usa document.registerElement() en lugar de customElements.define(). Hoy se considera una versión en desuso del estándar, llamada v0. customElements.define() es la nueva novedad y lo que los proveedores de navegadores están comenzando a implementar. Se llama Custom Elements v1.

Si te interesa la especificación anterior de v0, consulta el artículo html5rocks.

Navegadores compatibles

Chrome 54 (estado), Safari 10.1 (estado) y Firefox 63 (estado) tienen Elementos personalizados v1. Edge comenzó el desarrollo.

Para detectar elementos personalizados, verifica la presencia de window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

Hasta que haya compatibilidad general con navegadores, hay un polyfill independiente disponible para Custom Elements v1. Sin embargo, te recomendamos que uses el cargador de webcomponents.js para cargar de manera óptima los polyfills de los componentes web. El cargador usa la detección de atributos para cargar de forma asíncrona solo los rellenos de polimorfismo necesarios que requiere el navegador.

Instálalo:

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>

Conclusión

Los elementos personalizados nos proporcionan una nueva herramienta para definir etiquetas HTML nuevas en el navegador y crear componentes reutilizables. Cuando se combinan con las demás primitivas de la nueva plataforma, como Shadow DOM y <template>, se puede comenzar a ver el panorama general de Web Components:

  • Es compatible con varios navegadores (estándar de la Web) para crear y extender componentes reutilizables.
  • No requiere una biblioteca ni un framework para comenzar. ¡JS/HTML clásicos por la victoria!
  • Proporciona un modelo de programación conocido. Es solo DOM/CSS/HTML.
  • Funciona bien con otras funciones nuevas de la plataforma web (Shadow DOM, <template>, propiedades personalizadas de CSS, etc.).
  • Se integra por completo con DevTools del navegador.
  • Aprovecha las funciones de accesibilidad existentes.