Elementos personalizados v1: Componentes web reutilizables

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

Con los elementos personalizados, los desarrolladores web pueden crear nuevas etiquetas HTML, reforzar las etiquetas HTML existentes o extender los componentes que crearon otros desarrolladores. La API es la base de los componentes web. Aporta una forma web basada en estándares para crear componentes reutilizables con solo JS/HTML/CSS básicos. Como resultado, obtienes menos código y código modular, y una mayor reutilización en nuestras apps.

Introducción

El navegador nos brinda una excelente herramienta para estructurar aplicaciones web. Se llama HTML. ¡Seguro que la conoces! Es declarativo, portátil, compatible y fácil de usar. Por genial que sea el lenguaje HTML, su vocabulario y extensibilidad son limitados. Hasta ahora, el estándar HTML siempre careció de una forma de asociar automáticamente el comportamiento de JS con tu lenguaje de marcado.

Los elementos personalizados son la respuesta para modernizar el código HTML, completar las piezas faltantes y agrupar la estructura con el comportamiento. Si HTML no proporciona la solución a un problema, podemos crear un elemento personalizado que sí lo haga. Los elementos personalizados enseñan nuevos trucos al navegador y conservan los beneficios de HTML.

Cómo definir un nuevo elemento

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

El customElements global se usa para definir un elemento personalizado y enseñarle al navegador sobre una etiqueta nueva. Llama a customElements.define() con el nombre de etiqueta que deseas crear y un class de JavaScript que extienda el 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 usar un elemento personalizado no es diferente a usar un <div> o cualquier otro elemento. Las instancias se pueden declarar en la página y crearlas de forma dinámica en JavaScript, se pueden adjuntar objetos de escucha de eventos, etc. Sigue leyendo para obtener más ejemplos.

Cómo definir la API de JavaScript de un elemento

La funcionalidad de un elemento personalizado se define con una class de ES2015 que extiende HTMLElement. Extender HTMLElement garantiza que el elemento personalizado herede toda la API del DOM y significa que las propiedades o los métodos que agregas a la clase se vuelven parte de la interfaz del DOM del elemento. En esencia, usa la clase para crear una API pública de JavaScript para la etiqueta.

Ejemplo: Define la interfaz del 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 se refiere al elemento DOM en sí, es decir, la instancia de la clase. En nuestro ejemplo, this hace referencia a <app-drawer>. Así es como el elemento puede adjuntarse un objeto de escucha click a sí mismo. Además, no estarás limitado a objetos de escucha de eventos. La API completa del DOM está disponible dentro del código del elemento. Usa this para acceder a las propiedades del elemento, inspeccionar sus elementos secundarios (this.children), consultar nodos (this.querySelectorAll('.items')), etcétera.

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 todos nombres válidos, mientras que <tabs> y <foo_bar> no. Este requisito tiene el objetivo de que el analizador de HTML pueda distinguir los elementos personalizados de los elementos normales. También garantiza la compatibilidad con versiones futuras cuando se agreguen etiquetas nuevas al HTML.
  2. No puedes registrar la misma etiqueta más de una vez. Si intentas hacerlo, se arrojará una DOMException. Una vez que hayas informado al navegador sobre la nueva etiqueta, eso es todo. No hay devoluciones.
  3. Los elementos personalizados no pueden cerrarse automáticamente porque HTML solo permite que algunos elementos se cierren automáticamente. Escribe siempre una etiqueta de cierre (<app-drawer></app-drawer>).

Reacciones de elementos personalizados

Un elemento personalizado puede definir hooks especiales de ciclo de vida para ejecutar código durante momentos interesantes de su existencia. que se denominan 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 sobre 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 enumerados en la propiedad observedAttributes recibirán esta devolución de llamada.
adoptedCallback El elemento personalizado se movió a un nuevo document (p.ej., alguien llamado 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á inmediatamente a attributeChangedCallback(). De manera similar, recibirás un disconnectedCallback() inmediatamente después de que se quite tu elemento del DOM (p.ej., el usuario llama a el.remove()).

Ejemplo: Agrega 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 a IndexedDB en connectedCallback(), realiza el trabajo de limpieza necesario en disconnectedCallback(). Pero ten cuidado. No puedes confiar en que se quite tu elemento del DOM en todas las circunstancias. Por ejemplo, nunca se llamará a disconnectedCallback() si el usuario cierra la pestaña.

Propiedades y atributos

Refleja propiedades en atributos

Es común que las propiedades HTML reflejen su valor en el DOM como un atributo HTML. Por ejemplo, cuando los valores de hidden o id se cambian 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 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 ciertas APIs, como los selectores de CSS y accesibilidad, dependen de que funcionen los atributos.

Reflejar una propiedad resulta útil en cualquier lugar en el que desees mantener la representación del elemento DOM sincronizada con su estado de JavaScript. Una de las razones por las que quizás quieras reflejar una propiedad es que el estilo definido por el usuario se aplique cuando cambie el estado de JS.

Recuerda nuestro <app-drawer>. Un consumidor de este componente puede desear atenuarlo o evitar la interacción del usuario cuando está inhabilitado:

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

Cuando se cambia la propiedad disabled en JS, queremos que ese atributo se agregue al DOM para 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 son una forma conveniente para que los usuarios declaren el estado inicial:

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

Los elementos pueden reaccionar a los cambios de atributos mediante la definición de un attributeChangedCallback. El navegador llamará a este método para cada cambio en los atributos enumerados 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 establecen atributos adicionales en el <app-drawer> cuando se cambia un atributo disabled. Aunque no lo hacemos aquí, también puedes usar el attributeChangedCallback para mantener una propiedad de JS sincronizada con su atributo.

Actualizaciones de elementos

HTML mejorado de forma progresiva

Ya aprendimos que los elementos personalizados se definen llamando a customElements.define(). Sin embargo, esto no significa que debas definir y registrar un elemento personalizado de una sola vez.

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

La mejora progresiva es una característica de los elementos personalizados. En otras palabras, puedes declarar varios 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 trata los posibles elementos personalizados de manera diferente gracias a las etiquetas desconocidas. El proceso de llamar a define() y otorgar 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 el elemento

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

Ejemplo: Crea un elemento con algún código 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ó la muestra de código porque usaba controladores de eventos intercalados

Cómo crear un elemento que use Shadow DOM

Shadow DOM proporciona una forma para que un elemento posea una parte del DOM independiente del resto de la página, la represente y 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

Crea elementos a partir de un <template>

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

Ejemplo: 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 las cosas clave que suceden:

  1. Definimos un nuevo elemento en HTML: <x-foo-from-template>
  2. El Shadow DOM del elemento se crea a partir de un objeto <template>.
  3. El DOM del elemento es local para el elemento gracias al Shadow DOM.
  4. El CSS interno del elemento se define en función del elemento gracias al Shadow DOM.

Estoy en Shadow DOM. Mi lenguaje de marcado se selló de una <template>.

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

Cómo aplicar diseño a un elemento personalizado

Incluso si tu elemento define su propio estilo con Shadow DOM, los usuarios pueden diseñar 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ás te preguntes cómo funciona la especificidad de 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 Cómo crear un elemento que use Shadow DOM.

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

Antes de actualizar un elemento, puedes segmentarlo en CSS mediante la seudoclase :defined. Esto es útil para definir previamente un componente. Por ejemplo, es posible que desees evitar el diseño u otro FOUC visual ocultando los componentes no definidos y atenuándolos de forma gradual cuando se definan.

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

Después de definir <app-drawer>, el selector (app-drawer:not(:defined)) ya no coincide.

Extiende elementos

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

Extiende 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 mediante elementos personalizados.

Un elemento personalizado integrado es aquel que extiende una de las etiquetas HTML integradas del navegador. El beneficio principal de extender un elemento existente es obtener todas sus funciones (propiedades del DOM, métodos y accesibilidad). No hay mejor forma de escribir una app web progresiva que mejorar de manera progresiva los elementos HTML existentes.

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

Ejemplo: Se extiende <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 un poco cuando se extiende un elemento nativo. El tercer parámetro obligatorio le indica al navegador qué etiqueta estás extendiendo. Esto es necesario porque muchas etiquetas HTML comparten la misma interfaz del DOM. <section>, <address> y <em> (entre otros) comparten HTMLElement; tanto <q> como <blockquote> comparten HTMLQuoteElement, etc. Si especificas {extends: 'blockquote'}, el navegador sabrá que estás creando un <blockquote> actualizado en lugar de <q>. Consulta las especificaciones de HTML para ver la lista completa de interfaces DOM del HTML.

Los usuarios de un elemento integrado personalizado pueden usarlo de varias maneras. Para declararlo, se debe agregar el atributo is="" a 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: Se extiende <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">

o crea 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);

Otros detalles

Comparación entre elementos desconocidos y elementos personalizados indefinidos

El lenguaje HTML es flexible para trabajar con él. Por ejemplo, si declaras <randomtagthatdoesntexist> en una página, el navegador lo aceptará sin problemas. ¿Por qué funcionan las etiquetas no estándar? La respuesta es que la especificación 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 HTMLElement si se crean con un nombre válido (incluido un "-"). Puedes verificar 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 customElements global 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 de elemento personalizado válido, se muestra el constructor del elemento. Muestra undefined si no se registró una definición de 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 asistencia del navegador

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

Si te interesa la especificación anterior de la versión 0, consulta el artículo de 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 existencia de window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

Hasta que la compatibilidad con navegadores esté ampliamente disponible, hay un polyfill independiente disponible para los elementos personalizados 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 funciones para cargar de forma asíncrona solo los polyfills 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. Combínalos con las otras primitivas de la plataforma nueva, como Shadow DOM y <template>, y comenzarás a obtener el panorama general de los componentes web:

  • 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ásico por la victoria!
  • Proporciona un modelo de programación conocido. Es solo DOM/CSS/HTML.
  • Funciona bien con otras funciones nuevas de plataformas web (Shadow DOM, <template>, propiedades personalizadas de CSS, etcétera).
  • Se integra estrechamente con Herramientas para desarrolladores del navegador.
  • Aprovecha las funciones de accesibilidad existentes.