Seleccionar página
HTMLElement es una de las piezas clave de los Web Component, pero en general se pasa muy rápidamente por sus características, utilizándolo como un mero contenedor de los elementos de nuestro componente. Os proponemos dar un repaso a fondo a sus capacidades a fin de descubrir cómo sacarle todo el partido.

Características heredadas de HTMLElement

Cuando creamos una clase heredando de HTMLElement para crear un Web Component, hemos obtenido una cantidad enorme -casi brumadora- de métodos, propiedades, atributos y eventos.

La cadena de clases de la que estamos heredando es bastante considerable, ya que:

  • Nosotros heredamos de HTMLElement
    • que hereda de Element,
      • que hereda de Node
        • que hereda deEventTarget
          • que hereda de Object

Cada uno de ellos aportan un buen número de características que podemos aprovechar. Vamos a revisarlas…

Métodos

Cualquier componente web, al heredar de HTMLElement, y su cadena de clases padre, dispone de los siguientes métodos:

addEventListener(), after(), animate(), append(), appendChild(), attachShadow(), before(), blur(), click(), cloneNode(), closest(), compareDocumentPosition(), computedStyleMap(), contains(), createShadowRoot(), dispatchEvent(), focus(), hasAttribute(), hasAttributeNS(), hasAttributes(), hasChildNodes(), hasPointerCapture(), insertAdjacentElement(), insertAdjacentHTML(), insertAdjacentText(), insertBefore(), isDefaultNamespace(), isEqualNode(), isSameNode(), lookupNamespaceURI(), lookupPrefix(), matches(), normalize(), prepend(), querySelector(), querySelectorAll(), releasePointerCapture(), remove(), removeAttribute(), removeAttributeNode(), removeAttributeNS(), removeChild(), removeEventListener(), replaceChild(), replaceWith(), requestFullscreen(), requestPointerLock(), scroll(), scrollBy(), scrollIntoView(), scrollIntoViewIfNeeded(), scrollTo() y toggleAttribute()

Propiedades

En cuanto a propiedades, están a nuestra disposición:

CDATA_SECTION_NODE, COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, DOCUMENT_POSITION_CONTAINED_BY, DOCUMENT_POSITION_CONTAINS, DOCUMENT_POSITION_DISCONNECTED, DOCUMENT_POSITION_FOLLOWING, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, DOCUMENT_POSITION_PRECEDING, DOCUMENT_TYPE_NODE, ELEMENT_NODE, PROCESSING_INSTRUCTION_NODE, TEXT_NODE, accessKey *, assignedSlot, attributes, autocapitalize *, baseURI, childElementCount, childNodes, children, classList, className, clientHeight, clientLeft, clientTop, clientWidth, contentEditable *, dataset, dir, draggable *, firstChild, firstElementChild, hidden *, id *, innerHTML, innerText, inputMode *, isConnected, isContentEditable *, lang *, lastChild, lastElementChild, localName, namespaceURI, nextElementSibling, nextSibling, nodeName, nodeType, nodeValue, nonce, offsetHeight, offsetLeft, offsetParent, offsetTop, offsetWidth, outerHTML, outerText, ownerDocument, parentElement, parentNode, prefix, previousElementSibling, previousSibling, scrollHeight, scrollLeft, scrollTop, scrollWidth, shadowRoot, slot, spellcheck *, style *, tabIndex *, tagName, textContent, title * y translate *.

* son propiedades asociadas a un atributo.

Atributos

En nuestros Web Component, tenemos directamente disponibles los siguientes atributos:

accesskey, autocapitalize, class, contenteditable, contextmenu, data-*, dir, draggable, dropzone, hidden, id, inputmode (asociado a contenteditable), is, itemid, itemprop, itemref, itemscope, itemtype, lang, slot, spellcheck, style, tabindex, title y translate

Eventos

Algo parecido pasa con los eventos, HTMLElement y su cadena de clases padre están preparados para emitir eventos, sin que nosotros tengamos que hacer nada especial. Tenemos disponibles:

abort, auxclick, blur, cancel, canplay, canplaythrough, change, click, close, contextmenu, copy, cuechange, cut, dblclick, drag, dragend, dragenter, dragleave, dragover, dragstart, drop, durationchange, emptied, ended, error, focus, fullscreenchange, fullscreenerror, gotpointercapture, input, invalid, keydown, keypress, keyup, load, loadeddata, loadedmetadata, loadstart, lostpointercapture, mousedown, mouseenter, mouseleave, mousemove, mouseout, mouseover, mouseup, mousewheel, paste, pause, play, playing, pointercancel, pointerdown, pointerenter, pointerleave, pointermove, pointerout, pointerover, pointerup, progress, ratechange, reset, resize, scroll, search, seeked, seeking, select, selectionchange, selectstart, stalled, submit, suspend, timeupdate, toggle, volumechange, waiting y wheel

Para cada uno de estos eventos existe una propiedad que empieza con «on» que sirve para definir un manejador del evento, por ejemplo onclick. Estas propiedades existen por compatibilidad con versiones anteriores y es preferible utilizar .addEventListener() para definir un manejador asociado a un evento.

Ejemplo

Como ejemplo básico de lo que podemos conseguir con los atributos, métodos, propiedades y eventos obtenidos a partir de HTMLElement hemos preparado este código. Muestran un componente seleccionable con el tabulador (propiedad tabindex), y con las teclas Ctrl+Alt+S en Mac o Alt+S en Windows y Linux (propiedad accesskey), capturando un evento de tipo focus, que lanza cuando se obtiene el foco, por medio del método .addEventListener. Al producirse ese evento mostramos en consola un mensaje en el que usamos la propiedad .tagName.

<my-component id="test" tabindex="0" accesskey="s">Hello world (Alt+S)</my-component>
<script>
  class MyComponent extends HTMLElement {};
  
  customElements.define ('my-component', MyComponent);
  
  document.querySelector ('#test').addEventListener ('focus', function () {
    console.log (this.tagName, 'focus');
  });
</script>
 

Debemos ser conscientes de que todos estos métodos, propiedades, atributos y eventos están ahí, a nuestra disposición, y no tenemos que inventar de nuevo aquello que un componente basado en HTMLElement ya dispone por sí mismo.

Instancias de HTMLElement

Aunque pueda sorprendernos, no podemos crear instancias de HTMLElement, o de las clases que heredan de él, por medio de la instrucción new. Las únicas formas que tenemos de crear instancias son a través de registro de la clase con customElements.define() y la utilización del nombre del tag personalizado que hayamos definido con algunos de los siguientes mecanismos:

  • document.createElement(): si a este método le pasamos el nombre de custom tag creará una instancia de nuestro componente, que todavía no está conectada con el DOM.
  • Incluir el custom tag en el HTML también nos creará una instancia de nuestro componente, en este caso ya incluida en el DOM.
  • Clonar un elemento también crea una nueva instancia del componente

Podemos verlo en este código (donde hemos comentado las líneas que producen errores):

<script>
  class X extends HTMLElement {}
  // const n = new HTMLElement();     // Uncaught TypeError: Illegal constructor
  // const i = new X();               // Uncaught TypeError: Illegal constructor
  customElements.define('wc-test', X);
  const e = document.createElement('wc-test');  // Instancia creada sin conexión
  const c = e.cloneNode();                      // Instancia cread por clonación
</script>
<wc-test></wc-test>            <!-- instancia creada directamente en el HTML -->
 

Debemos recordar que si definimos un elemento antes de haber registrado el componente web, si está conectado con el DOM se produce un proceso de upgrade automático para convertirse en una instancia de nuestro componente; si está desconectado del DOM tendremos que utilizar customElements.define() para que se produzca esta transformación y sea una instancia efectiva de nuestro componente.

Si quieres conocer más sobre el objeto customElements te recomendamos que leas el artículo customElements a fondo donde te explicamos como sacarle todo el partido al registro de componentes web.

Ciclo de vida del componente

Si hemos utilizado alguno de los frameworks Javacript basados en componentes, quizás nos sorprenda saber que el ciclo de vida de los componentes web nativos es bastante sencillo y sólo disponemos de callbacks para capturar las situaciones de:

  • creación de instancia (constructor())
  • conexión con el DOM (connectedCallback())
  • desconexión del DOM (disconnectedCallback())
  • cambio de document (adoptedCallback())

Vamos a profundizar en cada uno de ellos…

constructor

Se ejecuta siempre que se crea una instancia del componente, independientemente de la forma en la que se haya creado, y si está o no conectada con el DOM. El constructor sólo se ejecuta una vez en la vida del componente.

<my-component></my-component>
<script>
  class MyComponent extends HTMLElement {
    constructor () {
      super ();
      console.log ('constructor of', this.tagName, ' isConnected', this.isConnected);
    }
  }
  customElements.define ('my-component', MyComponent);
  const element1 = document.querySelector('my-component').cloneNode();
  const element2 = document.createElement('my-component');
</script>
 

En la consola obtendremos un resultado de este tipo:

constructor of MY-COMPONENT  isConnected true
constructor of MY-COMPONENT  isConnected false
constructor of MY-COMPONENT  isConnected false

El primero de estos mensajes corresponde al tag <my-component> que se convierte en nuestro componente en cuanto este es definido con customElements.define().

El segundo de ellos corresponde a la instrucción document.querySelector('my-component').cloneNode(); que crea una copia del elemento y, por lo tanto, crea una nueva instancia de él, lanzando el constructor. Está copia está desconectada del DOM.

El último mensaje corresponde a la instrucción document.createElement('my-component'); que crea un elemento desconectado del DOM.

¿Por qué es tan importante si el componente está o no conectado al DOM? Si el componente no está conectado, el constructor tienen algunas limitaciones, por ejemplo, no puede escribir directamente otros elementos dentro del tag (lo que se denomina Light DOM) con una instrucción del tipo this.innerHTML, tampoco podrá preguntar por su padre con algo del tipo this.parentNode.

<my-component></my-component>
<script>
  class MyComponent extends HTMLElement {
    constructor () {
      super ();
      console.log ('constructor of', this.tagName, ' isConnected', this.isConnected);
      try {
        console.log ('created into', this.parentNode.tagName);
      } catch (err) {
        console.error (err.message);
      }
      try {
        this.innerHTML = '<h1>Hello</h1>'
      } catch (err) {
        console.error (err.message);
      }
    }
  }

  customElements.define ('my-component', MyComponent);
  const element1 = document.querySelector ('my-component').cloneNode ();
  const element2 = document.createElement ('my-component');
</script>

En este ejemplo se pueden ver algunos de los errores que podemos encontrarnos al intentar interactuar con el DOM desde un constructor de un elemento que no está conectado al DOM.

connectedCallback

Cuando un componente web pasa a estar conectado al DOM se ejecuta el método connectedCallback. Este es un método que podemos implementar en nuestra clase que hereda de HTMLElement y que es llamado internamente, de forma síncrona, cuando el elemento se conecta en alguna parte del document.

En el caso de instanciar el componente como una etiqueta HTML, se ejecuta el constructor e inmediatamente después el método connectedCallback:

<my-component></my-component>
<script>
  class MyComponent extends HTMLElement {
    constructor () {
      super ();
      console.log ('constructor of', this.tagName, 'isConnected', this.isConnected);
    }

    connectedCallback () {
      console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected);
    }
  }

  customElements.define ('my-component', MyComponent);
</script>

Si hemos clonado un componente o lo hemos creado con document.createElement(), entonces se ejecutará connectedCallback cuando se conecte al DOM por medio de document.body.append () o cualquier otra instrucción similar, pero no inmediatamente después de su creación.

class MyComponent extends HTMLElement {
  constructor () {
    super ();
    console.log ('constructor of', this.tagName, 'isConnected', this.isConnected);
  }

  connectedCallback () {
    console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected);
  }
}

customElements.define ('my-component', MyComponent);

const element = document.createElement('my-component');     // contructor
document.body.append(element);                              // connectedCallback
 

Es muy importante tener en cuenta que es posible que connectedCallback sea llamado más de una vez en el ciclo de vida de un componente. Por ejemplo, una operación de movimiento del componente web entre un elemento a otro del DOM lanzará este evento más de una vez.

<div id="base">
  <my-component></my-component>
</div>
<div id="next">
</div>
<script>
  class MyComponent extends HTMLElement {
    constructor () {
      super ();
      console.log ('constructor of', this.tagName, 'isConnected', this.isConnected);
    }

    connectedCallback () {
      console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected);
    }
  }

  customElements.define ('my-component', MyComponent);

  document.querySelector('#next').append(document.querySelector('my-component'));
</script>

En este ejemplo, la última instrucción mueve el elemento desde un DIV a otro dentro del DOM. Esta operación lanza de nuevo el método connectedCallback.

disconnectedCallback

Cuando un componente web está conectado al DOM y es desconectado se ejecuta el método disconnectCallback() de nuestro componente, es decir, si se saca del DOM, bien para retirarlo definitivamente, o bien para insertarlo en otro lugar, HTMLElement ejecuta, de forma síncrona, este callback.

<div id="base">
  <my-component></my-component>
</div>
<div id="next">
</div>
<script>
  class MyComponent extends HTMLElement {
    constructor () {
      super ();
      console.log ('constructor of', this.tagName, 'isConnected', this.isConnected);
    }

    connectedCallback () {
      console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected);
    }

    disconnectedCallback () {
      console.log ('disconnectedCallback of', this.tagName, 'isConnected', this.isConnected);
    }
  }

  customElements.define ('my-component', MyComponent);

  document.querySelector('#next').append(document.querySelector('my-component'));
</script>

En este ejemplo podemos observar que se ejecuta disconnectedCallback() antes de llamar por segunda vez a connectCallback().

Es importante saber que disconnectedCallback() es llamado antes de que el elemento esté desconectado y, por lo tanto, podemos operar con el resto de elementos del DOM sin limitaciones.

Tenemos que tener en cuenta que disconnectedCallback() puede ser llamado varias veces en el ciclo de vida del componente.

Es importante saber que si el usuario cierra la pestaña donde está nuestro componente, o cierra el navegador, no se va a ejecutar el método disconnectedCallback, por lo que no lo podemos utilizar como método de cierre o finalización de nuestro componente.

adoptedCallback

Aunque es una operación poco habitual, existe un callback específico que nos avisa si se ha cambiado el componente a otro documento, en ese caso se invoca, de forma síncrona, a adoptedCallback. No es sencillo mostrar casos prácticos donde esto sea relevante, pero vamos a intentar mostrar un ejemplo:

<iframe src="about:blank" id="frame"></iframe>

<my-component>Hello</my-component>

<script>
  class MyComponent extends HTMLElement {
    constructor () {
      super ();
      console.log ('constructor of', this.tagName);
    }
    
    connectedCallback (...args) {
      console.log ('connectedCallback of', this.tagName);
    }
    
    disconnectedCallback (...args) {
      console.log ('disconnectedCallback of', this.tagName);
    }
    
    adoptedCallback (...args) {
      console.log ('adoptedCallback of', this.tagName, 'with parameters', ...args);
    }
  }
  
  customElements.define ('my-component', MyComponent);
  
  const el = document.querySelector ('my-component');
  document.querySelector ('#frame').contentDocument.body.appendChild (el);
</script>
 

En este ejemplo tenemos un IFRAME al que movemos el componente, en cuyo caso los mensajes por consola será más o menos de este tipo:

constructor of MY-COMPONENT
connectedCallback of MY-COMPONENT
disconnectedCallback of MY-COMPONENT
adoptedCallback of MY-COMPONENT with parameters #document #document
connectedCallback of MY-COMPONENT

Este método recibe dos parámetros, en el primero hay una referencia al objeto document de origen y en el segundo una referencia al objeto document de destino. Como se puede observar, también se ejecutan los métodos disconnectCallback() antes de la adopción por el nuevo documento y connectedCallback() tras esta adopción.

Observación de atributos

Otra de las características que nos ofrece HTMLElement para es un mecanismo para observar los cambios en los atributos de la etiqueta HTML. Para ello tenemos que hacer dos cosas:

  • crear un un método getter estático con el nombre observedAttributes que devuelva una matriz con los nombres de las propiedades que se desea observar
  • crear un método que actúe como callback con el nombre attributeChangedCallback y que será llamado de forma síncrona cuando se cambie un atributo, recibiendo tres parámetros: el nombre del atributo, el valor anterior y el valor actual.

En este ejemplo registramos un observador al atributo num y lo modificamos con .setAttribute(), lo que produce una llamada a attributeChangedCallback():

<my-component num="0">Hello</my-component>

<script>
  class MyComponent extends HTMLElement {
    static get observedAttributes() {
      return ['num'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
      console.log ('attributedCallback with parameters', name, oldValue, newValue);
    }
  }

  customElements.define ('my-component', MyComponent);

  document.querySelector ('my-component').setAttribute('num', 10);
</script>

Es interesante observar en este ejemplo que el método attributeChangedCallback() es llamado dos veces, una primera con el valor inicial del atributo num que hemos incluido en la etiqueta HTML, recibiendo como valor anterior null y valor actual 0, y una segunda vez cuando ejecutamos el método .setAttribute() para actualizar su valor, donde recibimos el valor inicial y el nuevo valor.

Observación de propiedades

Si queremos observar los cambios en las propiedades de un componente web que hereda de HTMLElement, basta con que utilicemos un getter y un setter para controlar el acceso y la modificación de la propiedad.

Si la propiedad la hemos definido en el propio componente web, tendremos que almacenar los valores en el objeto con un Symbol o utilizar un WeakMap para guardar los datos privados de la clase.

<my-component>Hello</my-component>

<script>
  {
    const priv = new WeakMap ();

    class MyComponent extends HTMLElement {
      constructor () {
        super ();
        priv.set (this, {num : 0});
      }

      get num () {
        console.log ('get num', priv.get (this).num);
        return priv.get (this).num;
      }

      set num (value) {
        console.log ('set num', value);
        priv.get (this).num = value;
      }
    }

    customElements.define ('my-component', MyComponent);

    const el = document.querySelector ('my-component');
    console.assert (el.num === 0);
    el.num = 100;
    console.assert (el.num === 100);
  }
</script>

Si queremos capturar en nuestro componente propiedades definidas en HTMLElement debemos utilizar la instrucción super para realizar el cambio de forma efectiva en el componente.

<my-component id="a">Hello</my-component>
<my-component id="b">Hello</my-component>

<script>
  class MyComponent extends HTMLElement {
    get id () {
      console.log ('get id', super.id);
      return super.id;
    }

    set id (value) {
      console.log ('get id', value);
      super.id = value;
    }

    get innerHTML () {
      console.log ('get innerHTML', super.innerHTML);
      return super.innerHTML;
    }

    set innerHTML (value) {
      console.log ('set innerHTML', value);
      super.innerHTML = value;
    }
  }

  customElements.define ('my-component', MyComponent);

  const el1 = document.querySelector ('#a');
  el1.id    = 'c';

  const el2     = document.querySelector ('#b');
  el2.innerHTML = '<h2>Hello</h2>';

</script>

Sincronizar atributos y propiedades

Hemos visto atributos y propiedades, ahora vamos a ver cómo podemos mantener ambas sincronizadas. Es responsabilidad de nuestro código sincronizar o no las propiedades y atributos que hayamos definido. Es decir, que si queremos que en nuestro componente al actualizar una propiedad se actualice su atributo y cuando se actualice el atributo se actualice la propiedad, debemos implementarlo más o menos de esta forma:

<my-component num="0">Hello</my-component>

<script>
  {
    const priv = new WeakMap ();

    class MyComponent extends HTMLElement {
      static get observedAttributes () {
        return [ 'num' ];
      }

      constructor () {
        super ();
        priv.set (this, {});
      }

      attributeChangedCallback (name, oldValue, value) {
        console.log ('set attribute num', value);
        priv.get (this).num = parseInt (value);
      }

      get num () {
        console.log ('get num', priv.get (this).num);
        return priv.get (this).num;
      }

      set num (value) {
        console.log ('set num', value);
        priv.get (this).num = value;
        this.setAttribute ('num', value);
      }
    }

    customElements.define ('my-component', MyComponent);

    const el = document.querySelector ('my-component');

    console.assert (el.num === 0);                      // Valor inicial

    el.num = 100;                                       // Actualizar la propiedad
    console.assert (el.getAttribute ('num') === '100');  // Comprobamos el atributo

    el.setAttribute ('num', 300);                        // Actualizar el atributo
    console.assert (el.num === 300);                    // Comprobamos la propiedad
  }
</script>
 

Hay que tener en cuenta que las propiedades pueden tener un guion en el nombre (-) y las propiedades no, por lo que normalmente se realiza una transformación entre el nombre del atributo y el nombre de la propiedad. En este ejemplo se ha incluido una pequeña función para hacer esta trasformación del nombre.

HTMLElement vs HTMLUnkwonElement

Algo bastante curioso es que cuando en nuestro documento incluimos una etiqueta HTML que no existe, es decir, no está soportada por navegador y no ha sido definida como una custom tag, puede ocurrir dos cosas:

  • Que el nombre esté bien formado para ser aceptado como un custom tag, en cuyo caso se crea automáticamente como un elemento de tipo HTMLElement.
  • Que el nombre no puede ser asociado a un custom tag, por ejemplo, porque no incluye un guion (-), entonces es creado directamente como una instancia de HTMLUnknownElement.

De esta forma podemos tener componentes basados en HTMLElement sin necesidad de crear un componente personalizado.

<desconocido></desconocido>
<tag-desconocido></tag-desconocido>
<script>
  console.assert(
    document.querySelector('desconocido') instanceof HTMLUnknownElement
  );

  console.assert(
    document.querySelector('tag-desconocido') instanceof HTMLElement
  );
</script>

Recordemos que si un custom tag es registrado como posteriormente con customElement.define(), se produce un efecto upgrade y es convertido en una instancia del componente web correspondiente, con todos los métodos y propiedades que este tenga definidos.

Clases heredadas de HTMLElement

Antes de concluir es importante que diferenciemos dos tipos de Custom Tag que podemos utilizar, los basados en elementos concretos del HTML, normalmente denominados Built-in components y aquellos que están basados directamente en HTMLElement. Aunque tiene muchas cosas en común, también tienen importantes diferencias.

Built-in elements

Todos las etiquetas HTML soportadas por el navegador están basadas en HTMLElement y tienen esta clase en su cadena de prototipos. Por ejemplo, una etiqueta button es una instancia de la clase HTMLButtonElement, y esta a su vez hereda de HTMLElement.

Todo lo que hemos explicado sobre HTMLElement lo podemos aplicar a nuestros componentes predefinidos. De esta forma podemos heredar de estas clases y estaríamos creando un Customized built-in elements.

Cuando creamos un componente heredando de un componente predefinido del HTML, debemos tener en cuenta:

  • no es soportado por Safari y Opera, ni por los Polyfill disponibles (IE, Edget).
  • cuando se registra con customElement.define() hay que pasar un tercer parámetro indicando el elemento que estamos extendiendo, por ejemplo, {extends: button'}
  • Para crear un elemento de este tipo debemos utilizar la propiedad is, por ejemplo <button is="my-button"></button>.

Vamos a mostrar un ejemplo donde definimos un Customized Built-in Component y utilizamos varias de las características que hemos ido describiendo hasta ahora de HTMLElement:

<label is="wc-blink" base-color="#000000" alternative-color="#FF0000" change-interval="2">
Hello
</label>
<label is="wc-blink">world</label>
<script>
  {
    const attr2prop = (name) => name
      .replace (/-([a-z]|[A-Z])/g, (c) => c.toUpperCase ())
      .replace (/-/g, '');
    const priv      = new WeakMap ();

    class Blink extends HTMLLabelElement {
      static get observedAttributes () {
        return [ 'base-color', 'alternative-color', 'change-interval' ];
      }

      constructor () {
        super ();
        priv.set (this, {
          changeInterval   : 1000,
          baseColor        : 'inherit',
          alternativeColor : 'transparent'
        });
      }

      connectedCallback () {
        let n      = 0;
        const that = this;
        (function show () {
          that.style.color = ++n % 2 ? that.alternativeColor : that.baseColor;
          setTimeout (show, parseInt (that.changeInterval) || 1000);
        }) ();
      }

      attributeChangedCallback (name, oldValue, value) {
        const data               = priv.get (this);
        data[ attr2prop (name) ] = name === 'change-interval' ? value * 1000 : value;
      }

      get baseColor () {
        return priv.get (this).baseColor;
      }

      set baseColor (value) {
        this.setAttribute ('baseColor-color', value);
      }

      get alternativeColor () {
        return priv.get (this).alternativeColor;
      }

      set alternativeColor (value) {
        this.setAttribute ('alternativeColor-color', value);
      }

      get changeInterval () {
        return priv.get (this).changeInterval;
      }

      set changeInterval (value) {
        this.setAttribute ('change-changeInterval', (parseInt (value) / 1000).toString ());
      }

    }

    window.customElements.define ('wc-blink', Blink, {extends : 'label'});
  }
</script>

Lamentablemente, la falta de soporte y la poca elegancia de la cláusula is hacen que no sea muy habitual utilizar los Customized build-in elements y se tiene a definir un componente autónomo que encapsula el componente predefinido, en vez de extender los componentes predefinidos.

Custom element autónomos

Habitualmente nuestros componentes serán clases que heredan directamente de HTMLElement, lo que se denomina Autonomous custom elements. Es decir, son componentes web autónomos que contienen todos los elementos que necesitan para su renderización, ya sea en el light DOM o en shadow DOM, y que no heredan de los componentes Built-in. Estos componentes son mucho más elegantes de utilizar, al registrarse un nombre específico para el tag, y pueden contener cualquier elemento predefinido, e incluso otros componentes web.

En la práctica, podemos establecer los niveles de herencia que queramos, sólo es necesario que en la cadena de prototipos aparezca HTMLElement y no uno de los componentes predefinidos del HTML. Existen muchas aproximaciones diferentes sobre cómo estructurar un modelo de clases y sus herencias para facilitar la construcción de un sistema o, incluso, framework de componentes web.

A modo de ejemplo, vamos a crear una clase BiColor que gestione los atributos base-color y alternative-color y dos clases derivadas, una que cambia el color cada un intervalo de tiempo y otro que cambiar entre los colores cuando se pasa el ratón por encima del componente. No tienen utilidad alguna, pero nos muestran las capacidades de la herencia entre componentes.

<wc-blink base-color="#000000" alternative-color="#FF0000" change-interval="2">
Hello
</wc-blink>
<wc-blink>word:</wc-blink>
<wc-hover alternative-color="#0000FF">move mouse over this text</wc-hover>
<script>
  {
    
    const attr2prop = (name) => name
      .replace (/-([a-z]|[A-Z])/g, (c) => c.toUpperCase ())
      .replace (/-/g, '');
    const priv      = new WeakMap ();
    
    
    class BiColor extends HTMLElement {
      static get observedAttributes () {
        return [ 'base-color', 'alternative-color' ];
      }
      
      constructor () {
        super ();
        priv.set (this, {
          baseColor        : 'inherit',
          alternativeColor : 'transparent'
        });
      }
      
      attributeChangedCallback (name, oldValue, value) {
        priv.get (this)[ attr2prop (name) ] = value;
      }
      
      get baseColor () {
        return priv.get (this).baseColor;
      }
      
      set baseColor (value) {
        this.setAttribute ('baseColor-color', value);
      }
      
      get alternativeColor () {
        return priv.get (this).alternativeColor;
      }
      
      set alternativeColor (value) {
        this.setAttribute ('alternativeColor-color', value);
      }
      
    }
    
    class Blink extends BiColor {
      static get observedAttributes () {
        return [ 'change-interval' ].concat (super.observedAttributes);
      }
      
      constructor () {
        super ();
        priv.get (this).changeInterval = 1000;
      }
      
      connectedCallback () {
        let n      = 0;
        const that = this;
        (function show () {
          that.style.color = ++n % 2 ? that.alternativeColor : that.baseColor;
          setTimeout (show, parseInt (that.changeInterval) || 1000);
        }) ();
      }
      
      attributeChangedCallback (name, oldValue, value) {
        if (name === 'change-interval') {
          priv.get (this)[ attr2prop (name) ] = value * 1000;
        } else {
          super.attributeChangedCallback (name, oldValue, value);
        }
      }
      
      get changeInterval () {
        return priv.get (this).changeInterval;
      }
      
      set changeInterval (value) {
        this.setAttribute ('change-changeInterval', (parseInt (value) / 1000).toString ());
      }
      
    }
    
    class Hover extends BiColor {
      static get observedAttributes () {
        return super.observedAttributes;
      }
      
      constructor () {
        super ();
        this.addEventListener ('mouseover', () => {
          this.style.color = this.alternativeColor;
        });
        this.addEventListener ('mouseout', () => {
          this.style.color = this.baseColor;
        });
      }
      
    }
    
    window.customElements.define ('wc-blink', Blink);
    window.customElements.define ('wc-hover', Hover);
  }
</script>

Aprovechando este ejemplo algo más complejo, podemos señalar que la propiedad estática observedAttributes se puede componer en diferentes niveles de herencia por medio de super.observedAttributes, junto con la llamada a super.attributeChangedCallback(). De esta forma las clases hijas llaman a los métodos y propiedades de las clases padre, permitiendo que sean registrados todos los atributos que deseamos observar en diferentes niveles.

Conclusiones

HTMLElement es uno de los principales pilares de los componentes web. Saber cómo funciona, que capacidades y que limitaciones tiene es fundamental para poder sacarle todo el partido a esta importante característica del desarrollo moderno en front.
Son muchos los frameworks que amplían las capacidades de los componentes nativos, creando sus propios ciclos de vida y añadiendo algunas características adicionales, pero los componentes web por si solos ofrecen una amplia gama de funcionalidades que podemos utilizar directamente en los navegadores modernos para realizar aplicaciones complejas que se aprovechen de la encapsulación y composición.

Novedades

Datos inmutables en Javascript

Datos inmutables en Javascript

En Javascript todo parece mutable, es decir, que se puede cambiar, pero lo cierto es que también nos ofrece varios mecanismos para conseguir que los datos que manejamos, especialmente los objetos, sean inmutables. Te invitamos a descubrir cómo…

Copiar objetos en Javascript

Copiar objetos en Javascript

Copiar objetos no es algo sencillo, incluso se podría decir que en si mismo no es posible, ya que el concepto «copiar» no entra dentro del paradigma de los objetos. No obstante, por medio de instrucciones como Object.assign() hemos aprendido como obtener objetos con las mismas propiedades, pero está técnica no se puede aplicar a todos los tipos de objetos disponibles en Javascript. Vamos a ver cómo podemos copiar cualquier tipo de objeto…

Descubre los Javascript Array Internals

Descubre los Javascript Array Internals

El Array es una de las estructuras más utilizadas en Javascript y no siempre bien comprendida. Hoy os invitamos a analizar el comportamiento interno de este objeto y descubrir cómo Javascript implementa las diferente acciones con los Array y que operaciones internas se realizan en cada caso.

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.