Seleccionar página
Hoy os invitamos a profundizar en el uso de customElements, pieza clave para la creación de Custom Tag, uno de los pilares de los Web Components. Cuando nos adentramos en el mundo de los Web Components solemos encontrar con facilidad artículos y referencias básicas e introductorias. Cuando queremos profundizar un poco más, la información empieza a ser más escasa y difícil de encontrar. Por ello vamos a intentar que, partiendo de lo más básico, podamos llegar a profundizar lo suficiente como para descubrir algunas características avanzadas de los componentes web.

Instancia de CustomElementRegistry

En primer lugar, debemos saber que la clase CustomElementRegistry es quien gestiona el registro de Custom Tag que utiliza el navegador cuando creamos Web Components. Por ello es un elemento muy importante que debemos conocer a fondo.

Para simplificar el acceso a este registro se ha definido el objeto window.customElements, que es una instancia del objeto window.CustomElementRegistry. Por lo tanto, esta instrucción devuelve true:

window.customElements instanceof window.CustomElementRegistry

Todo nuestro código trabajará sobre customElements y no vamos a utilizar directamente CustomElementRegistry. Seguramente nos sorprendamos un poco cuando descubramos que de esta clase CustomElementRegistry no se puede instanciar en nuevos objetos y que, si intentamos ejecutar un código de este tipo, obtendremos un error:

const newCustomElements = new CustomElementRegistry ();
// Uncaught TypeError: Illegal constructor

Nota: aunque todavía falta tiempo para que podamos utilizar CustomElementRegistry como un constructor más, ya os podemos adelantar que se está trabajando en una propuesta para utilizar este mecanismo para crear registros de componentes web locales a un Shadow Root y no tener que utilizar un único registro global, con los conflictos que eso conlleva.

Soporte y Polyfill

CustomElementRegistry y customElements están disponibles en:

Chrome 67 y Firefox 63.

Con un soporte limitado, ya que no permiten extensiones de componentes predefinidos (lo veremos más adelante) en:

Safari 10.3 y Opera 41.

Lamentablemente no está soportado en:

Internet Explorer y Edge (está previsto para la versión 76).

En aquellos casos donde no hay soporte a esta característica podemos intentar utilizar un Polyfill de Custom Elements.

Este polyfill lo instalamos con:

npm install @webcomponents/custom-elements

Para incluirlo en nuestro programa tenemos que cargarlo más o menos de esta forma:

<script src="custom-elements.min.js"></script>

También deberemos tener en cuenta el soporte para clases de ES6, ya que los Web Components los vamos a implementar como clases y necesitamos que el navegador tenga soporte para esta característica de Javascript.

Miembros de customElements

Si inspeccionamos un poco este objeto podemos ver todos sus métodos:

    Vamos a revisarlos uno a uno…

    customElements.define (nombre, clase [, opciones])

    Probablemente es el método más conocido de customElements, ya que es el que utilizamos para añadir nuevos Custom Tag en el registro general de componentes web del navegador.

    Este es un ejemplo básico de su utilización:

    <wc-blink base-color="#000000" alternative-color="#FF0000" change-interval="2">Hello</wc-blink>
    <wc-blink>by code</wc-blink>
    <script>
      class Blink extends HTMLElement {
        constructor () {
          super ();
          const interval = (this.getAttribute ('change-interval') || 1) * 1000;
          const base     = this.getAttribute ('base-color') || 'inherit';
          const alte     = this.getAttribute ('alternative-color') || 'transparent';
          let n          = 0;
          setInterval (() => {
            this.style.color = ++n % 2 ? alte : base;
          }, interval);
        }
      }
      customElements.define ('wc-blink', Blink);
    </script>
    

    Vamos a ver en detalle sus parámetros…

    nombre

    Como podemos observar, el primer parámetro del método customElements.define() es el nombre de tag que vamos a registrar y que es el que utilizaremos en el HTML.

    Para que sea válido, este nombre debe seguir unas reglas muy concretas:

    • Debe empezar por una letra entre la a y la z en minúsculas (no puede ser ñ o una vocal acentuada).
    • Debe contener un guión -
    • No puede utilizar letras mayúsculas

    A partir de estas reglas, se pueden añadir todo tipo de caracteres Unicode, pero siempre y cuando no coincidan con las siguientes palabras reservadas:

    annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name y missing-glyph

    Este tipo de nomenclatura evita coincidir con otros tags definidos en el HTML, por lo que no vamos a conseguir sustituir un tag ya existente por un Custom Tag.

    nombre único

    Lamentablemente una vez registrado un componente, no podemos volverlo a registrar con otra clase, es decir, no es posible registrar dos veces un componente con el mismo nombre y si lo intentamos se producirá un error del tipo «Uncaught DOMException: Failed to execute ‘define’ on ‘CustomElementRegistry’: the name «XX-XXXX» has already been used with this registry».

    Es importante tener cuidado con los nombres de los componentes, sobre todo en proyectos de cierta envergadura, donde diferentes módulos pueden intentar registrar el mismo nombre de componente más de una vez. Como veremos más adelante, podemos utilizar customElements.get() para comprobar si ya se ha registrado el nombre que queremos utilizar, pero no evita el conflicto de nombres.

    clase

    Autonomous custom element vs Customized built-in element

    Como segundo parámetro se debe incluir un constructor o una clase que en su cadena de prototipos herede o bien de HTMLElement o de algunos de los elementos que heredan de él. En este sentido nos encontramos con dos custom elements muy diferentes:

    • Autonomous custom element: son componentes que heredan del componente genérico HTMLElements.
    • .

    • Customized built-in element: son componentes que heredan de componentes concretos del HTML, por ejemplo, HTMLLabelElement.

    Estos dos modelos son completamente diferentes, el primero -extender del componente genérico HTMLELement-es sin duda el más utilizado y representa el caso general de componente web. El segundo -extender un componente HTML concreto -, aunque pueda parecer muy interesante, tiene bastante limitaciones, además de que no es soportado por Safari y Opera, ni por los Polyfill disponibles.

    Este es un ejemplo de extensión de Customized built-in element:

    <label is="wc-blink" base-color="#000000" alternative-color="#FF0000" change-interval="2">
    Hello
    </label>
    <label is="wc-blink">by default</label>
    <script>
      class Blink extends HTMLLabelElement {
        constructor () {
          super ();
          const interval = (this.getAttribute ('change-interval') || 1) * 1000;
          const base     = this.getAttribute ('base-color') || 'inherit';
          const alte     = this.getAttribute ('alternative-color') || 'transparent';
          let n          = 0;
          setInterval (() => {
            this.style.color = ++n % 2 ? alte : base;
          }, interval);
        }
      }
      customElements.define ('wc-blink', Blink, {extends: 'label'});
    </script>
    

    Como se puede observar, en este caso hemos definido un tipo de label que deberemos indicar con el atributo is="wc-blink", además de tener que pasar un tercer parámetro al método customElements.define().

    La mayoría de las veces se opta por definir un componente autónomo que encapsula el componente predefinido, en vez de extender los componentes predefinidos, por lo que no se está extendiendo el uso de customized built-in element .

    Siempre se debe heredar de HMTLElement

    El registro comprueba que todos los custom elements heredan directa o indirectamente de HTMLElement, si esto no es así, a la hora de instanciar el objeto se lanzará una excepción. En este mismo sentido, en nuestro constructor no se puede devolver otro objeto, por ejemplo, un Proxy. El registro de custom elements detecta que estamos devolviendo un objeto diferente al que espera y lanza un error.

    Ei podemos ampliar la cadena de prototipos con clases intermedias, es decir, no es necesario heredar directamente de HTMLElement, por ejemplo, podemos heredar de nuestra clase MyWebComponent y esta a su vez de HTMLElement.

    <wc-blink base-color="#000000" alternative-color="#FF0000" change-interval="2">Hello</wc-blink>
    <wc-blink>by code</wc-blink>
    <script>
      class MyWebComponent extends HTMLElement {
        constructor() {
          super();
          this.isMyWebComponet = true;
        }
      }
      class Blink extends MyWebComponent {
        constructor () {
          super ();
          const interval = (this.getAttribute ('change-interval') || 1) * 1000;
          const base     = this.getAttribute ('base-color') || 'inherit';
          const alte     = this.getAttribute ('alternative-color') || 'transparent';
          let n          = 0;
          setInterval (() => {
            this.style.color = ++n % 2 ? alte : base;
          }, interval);
        }
      }
      window.customElements.define ('wc-blink', Blink);
    </script>
    

    Si quieres conocer algo más sobre este componente, te recomendamos que consultes el artículo HTMLElement a fondo donde profundizamos un poco más sobre sus características y funcionalidades.

    No se puede utilizar la misma clase para dos Custom Tag

    La clase no se puede utilizar dos veces para dos Custom Tag diferentes. Esto es un poco desconcertante. Es más fácil comprender que no se puede utilizar el mismo nombre para dos Custom Tag, pero tampoco podemos utilizar la misma clase para dos tag diferentes.

    La buena noticia es que es muy fácil resolver esta limitación, basta con heredar de la clase, aunque no extendamos ninguna de sus características, para que obtengamos otra clase diferente y podamos utilizarla sin problemas:

    <wc-blink base-color="#000000" alternative-color="#FF0000" change-interval="2">Hello</wc-blink>
    <wc-blink-two>by code</wc-blink-two>
    <script>
      class Blink extends HTMLElement {
        constructor () {
          super ();
          const interval = (this.getAttribute ('change-interval') || 1) * 1000;
          const base     = this.getAttribute ('base-color') || 'inherit';
          const alte     = this.getAttribute ('alternative-color') || 'transparent';
          let n          = 0;
          setInterval (() => {
            this.style.color = ++n % 2 ? alte : base;
          }, interval);
        }
      }
      window.customElements.define ('wc-blink', Blink);
      window.customElements.define ('wc-blink-two', class extends Blink {});
    </script>
    

    Podemos envolver la clase con un Proxy

    La clase puede ser sustituida por un Proxy, lo cual puede resultar bastante interesante para hacer algunos dinamismos avanzados, ya que podemos capturar cuando se llama al constructor y algunas otras operaciones con la clase.

    Un momento, ¿no hemos dicho hace unos pocos párrafos que no se podía utilizar un Proxy? Lo aclaramos: no es posible devolver un proxy como objeto instanciado de la clase, pero la clase como tal si puede ser un Proxy. Vamos a ver cada caso para comprenderlo mejor:

    Un constructor no puede devolver un proxy, ya que el registro de componentes web se da cuenta de ello y lanza un error «Uncaught DOMException: custom element constructors must call super() first and must not return a different object». Por ejemplo:

    <wc-test>Hello</wc-test>
    <script>
      class Test extends HTMLElement {
        constructor () {
          return new Proxy (this, {
            get (...args) {
              console.log ('get', ...args);
              return Reflect.get (...args);
            }
          });
        }
      }
      customElements.define('wc-test', Test);
    </script>
    

    Por el contrario, sí podemos envolver la clase con un Proxy. Por ejemplo:

    <wc-test>Hello</wc-test>
    <script>
      class Test extends HTMLElement {
        constructor() {
          super();
        }
      }
      const ProxyTest = new Proxy(Test, {
        construct(...args) {
          console.log('constructor', ...args);
          return Reflect.construct(...args);
        }
      });
      customElements.define('wc-test', ProxyTest);
    </script>
    

    Las dos acciones -aunque ambas usan un Proxy- son completamente diferentes: el Proxy de sobre la clase permite capturar sólo acciones sobre la propia clase, y no sobre las instancias de la misma, que es lo que nos gustaría poder hacer con un Proxy sobre el objeto que retorna el constructor.

    opciones

    customElements.define() tiene un tercer parámetro opcional que sólo se utilizar en el caso de los componentes de tipo customized built-in element, y que siempre debe ser un objecto con la propiedad extends y cuyo valor debe ser el nombre del tag que se está extendiendo.

    Como ya hemos comentado, la falta de soporte universal a los componentes de tipo customized built-in element y la poco atractiva sintaxis <label is="nombre-componente"> hacen que la mayoría de las veces se opte por utilizar un componente autónomo que encapsula componentes predefinidos, en vez de extender estos componentes predefinidos directamente.

    customElements.get (nombre)

    Este método devuelve el constructor asociado al Custom Tag cuyo nombre pasamos como parámetro. Si este nombre no ha sido registrado, devuelve undefined.

    Es bastante útil para comprobar si un Custom Tag ha sido registrado o no. Por ejemplo:

    if (customElements.get('wc-blink') === undefined) {
      customElements.define ('wc-blink', Blink);
    }
    

    También se puede utilizar para comprobar si el Custom Tag ha sido registrado con otro constructor o por nuestro constructor:

    if (customElements.get('wc-blink') !== Blink) {
      console.error('wc-blink is registred with other constructor');
    }
    

    Aunque sería bastante útil, nos tenemos un mecanismo para conocer todos los Custom Tag que han sido registrados y sólo disponemos de este mecanismo para preguntar por un elemento concreto. Más adelante veremos cómo superar esta limitación.

    customElements.whenDefined (nombre)

    En algunas ocasiones vamos a escribir en nuestro código HTML un tag con los nombres de los Custom Tag antes de que estos estén registrados. No es un problema, ya que cuando se definan, los tag serán modificados con el constructor que hemos registrado.

    Para poder recibir una notificación del momento donde un determinado tag es definido está el método customElements.whenDefined(). Este método devuelve una promesa que será resuelta cuando se realice la definición. Si ya estuviera definido, la promesa se resuelve inmediatamente.

    En este ejemplo, algo forzado, registramos el Custom Tag dos segundos después de la ejecución del programa. Hasta ese momento mantenemos los elementos ocultos y sólo se muestran en el momento en el que el registro ha sido efectivo.

    <wc-blink style="display: none" base-color="#000000" alternative-color="#FF0000" change-interval="2">
    Hello
    </wc-blink>
    <wc-blink style="display: none">by code</wc-blink>
    <script>
      customElements.whenDefined ('wc-blink').then (() => {
        document.querySelectorAll('wc-blink').forEach(el => el.style.display = 'inline-block');
      });
      class Blink extends HTMLElement {
        constructor () {
          super ();
          const interval = (this.getAttribute ('change-interval') || 1) * 1000;
          const base     = this.getAttribute ('base-color') || 'inherit';
          const alte     = this.getAttribute ('alternative-color') || 'transparent';
          let n          = 0;
          setInterval (() => {
            this.style.color = ++n % 2 ? alte : base;
          }, interval);
        }
      }
      setTimeout(() => {
        if (customElements.get ('wc-blink') === undefined) {
          customElements.define ('wc-blink', Blink);
        } else if (customElements.get ('wc-blink') !== Blink) {
          console.error ('wc-blink is registred with other constructor');
        }
      }, 2000);
    </script>
    

    El uso del método customElements.whenDefined() no está muy extendido, pero es un interesante recurso en bastante ocasiones.

    customElements.upgrade (nodo)

    Aunque normalmente utilizaremos directamente los Custom Tag como etiquetas en HTML, también podemos crearlos por medio de document.createElement("custom-tag"). Cuando hacemos la creación de esta forma, antes de la definición de Custom Tag, se crea un elemento provisional, pero que carece de toda la lógica del componente.

    Cuando registramos el componente se actualizarán todos los Custom Tag que estén asociados al documento, es decir, que ya están en el DOM, pero no se actualizan los elementos que han sido creados con document.createElement() y que todavía no han sido añadidos a document, lo cual puede ser un serio problema.

    En estos casos, aunque poco habituales, podemos hacer uso del método customElements.upgrade() para aplicar el constructor asociado al Custom Tag que hemos registrado con posterioridad a la creación del elemento. Vamos a ver un ejemplo:

    const elementA = document.createElement('wc-test');
    class Test extends HTMLElement {}
    customElements.define ('wc-test', Test);
    
    if (elementA instanceof Test) {
      console.log('yes');
    } else {
      console.log('no');
    }
    
    customElements.upgrade(elementA);
    if (elementA instanceof Test) {
      console.log('yes');
    } else {
      console.log('no');
    }
    

    Ese es un mecanismo que no vamos a utilizar muchas veces, pero es muy útil en algunas ocasiones, por lo que importante que lo conozcamos para evitar problemas.

    Extender customElements

    Aunque, de momento, no podemos heredar de CustomElementsRegistry o crear nuevas instancias para crear nuestros propios registros, podemos sustituir el objecto window.customElements con un objeto propio que, respetando la interfaz original, ofrezca algunas funcionalidades extendidas. Siempre hay que tener prevención a modificar objetos globales, pero si se hace con cuidado, es una excelente forma de ampliar la funcionalidad del navegador.

    Por ejemplo, en este caso hemos definido un objeto customElements con dos funcionalidades extendidas:

    • No da error si intentamos utilizar dos veces el mismo constructor, ya que, en el caso de que ya haya sido utilizado, crea una subclase dinámicamente.
    • .

    • Incluye un nuevo método customElements.tagNames() que devuelve un iterador con todos Custom Tag registrados. De esta forma podemos conocer con facilidad todos los elementos que contiene el registro.
    Object.defineProperty (window, 'customElements', {
      configurabe : true,
      enumerable  : true,
      get         : (() => {
        const customElements = window.customElements;
        const constructors   = new Map ();
        const names          = new Set ();
        return () => ({
          get         : customElements.get,
          upgrade     : customElements.upgrade,
          whenDefined : customElements.whenDefined,
          define (name, klass, options) {
            names.add (name)
            return customElements.define (
              name,
              constructors.has (klass) ? (class extends klass {}) : klass,
              options
            );
          },
          tagNames () {
            return names.values();
          }
        })
      }) ()
    });
    

    Este sería un ejemplo de su uso:

    class Blink extends HTMLElement {}
    window.customElements.define ('wc-blink', Blink);
    
    class Test extends HTMLElement {}
    window.customElements.define ('wc-test', Test);
    
    console.log (customElements.tagNames ());
    

    Este tipo de extensión podemos adaptarla a nuestras necesidades, y, si la cargamos antes de definir los Custom Tag, nos puede ofrecer mucha información de utilidad para nuestros programas.

    Conclusiones

    Hemos dado un repaso a customElements y todos sus métodos. Hemos intentado empezar por lo más básico e ir avanzando hasta describir algunos de sus comportamientos más complejos. De paso, hemos podido descubrir las diferencias entre Autonomous custom element y Customized built-in element, así como los problemas de conflicto de nombres que podemos encontrarnos en proyectos de cierta envergadura a la hora de registrar los componentes. Esperamos que esta revisión haya aclarado algunos puntos y nos resulte un poco más fácil sacarle todo el partido a los diferentes métodos que nos ofrece el registro de componentes web.

    Novedades

    Web Components: pasado, presente y futuro

    Web Components: pasado, presente y futuro

    Los Web Components aparecieron en el panorama de desarrollo hace ya bastante tiempo. Desde su presentación se les ha prestado mucha atención, pero lo cierto es que no han sido acogidos de forma generalizada, quizás por la difusión de nuevos y potentes frameworks. Nos preguntamos qué ha pasado con este estándar y, sobre todo, que puede pasar de aquí en adelante con el uso práctico de los componentes web.

    Adaptar un componente web para Microsoft Edge

    Adaptar un componente web para Microsoft Edge

    Si quieres que tus componentes web funcionen sin problemas en Microsoft Edge, es conveniente que conozcas algunas de sus carencias y cómo resolverlas. Edge tiene soporte nativo a algunas de las características clave de los web component, pero hay que utilizar polyfill en otras y hay que tener en cuenta algunas limitaciones. Próximamente Edge utilizará Chromium como base de su desarrollo, pero de momento, es necesario aplicar estas soluciones.

    12 pasos para construir un componente web

    12 pasos para construir un componente web

    Para conocer cómo se desarrolla realmente un componente web no puedes perderte esta descripción paso a paso de la construcción de un componente real y de cierta complejidad. Podrás descubrir cómo, sólo utilizando el estándar, se pueden hacer cosas muy interesantes y completas.

    ¿Qué pasa con import y los web components?

    ¿Qué pasa con import y los web components?

    Uno de los más controvertidos pilares de los componentes web ha sido el HTML Import. Considerado en estos momentos como una funcionalidad discontinuada, debemos conocer como sacar el máximo partido la instrucción import de Javascipt para gestionar la carga de nuestros componentes.