Seleccionar página
Microsoft Edge es un navegador moderno, con un rendimiento muy aceptable, herramientas de depuración avanzadas, un soporte bastante aceptable de la mayoría de los estándares, pero en el caso de los componentes web tenemos un problema: su soporte es bastante limitado, especialmente en dos aspectos fundamentales, los Custom Elements y el Shadow DOM.

Para que funcione tenemos que utilizar los polyfill que distribuye webcomponents.org y, lamentablemente, modificar algunas partes de nuestro código para ajustarnos las capacidades de estas librerías. Vamos a revisar todos estos cambios lo sobre el código de nuestro componente web todojs-multiselect, que explicamos en profundidad en un artículo anterior.

Cargar los Polyfill

Aunque podemos cargar los polyfill uno a uno, lo más cómodo es utilizar el loader que nos ofrecen los desarrolladores del equipo de Google Polymer. Este cargador se encarga de analizar el entorno, en este caso Microsoft Edge, ver que carencias hay y cargar sólo lo necesario para el navegador en el que nos encontramos. Como suele ser habitual, este polyfill debemos cargarlo antes de nuestros componentes. Aunque podemos descargarlos e incluirlos en nuestro servidor, también podemos utilizar el servicio de CDN de unpkg.com.

<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

Custom Elements Polyfill

El polyfill de Custom Elements funciona muy bien en Microsoft Edge. Permite simular el registro de los componentes y aplicar de forma consistente nuestra clase al componente definido. En la práctica los elementos se mantienen como HTMLUnknownElement para el navegador, pero no es algo que nos afecte. En nuestra experiencia no hemos tenido que hacer ninguna adaptación en el codigo de nuestros componentes web para que funcione todo correctamente.

Shadow DOM Polyfill

El polyfill de Shadow DOM realmente son dos, uno centrado en el DOM y otro en el CSS. Ambos utilizan una técnica que se ha venido a denominar Shady (ShadyDOM y ShadyCSS), que consiste en establecer clases e identificadores que aíslen los elementos que deberían estar en el Shadow DOM, pero que en la práctica están ubicados en el Light DOM. Realmente tienen bastantes limitaciones en Microsoft Edge y su funcionamiento no es del todo transparente para nuestro código.

En la práctica funciona dando un cambiazo a ambos elementos Light DOM y Shadow DOM. El contenido que hemos puesto dentro del Light DOM queda oculto, y el contenido de Shadow DOM está dentro de nuestro componente para que pueda ser renderizado. Lo que vemos en las herramientas de desarrollo es el Shadow DOM ubicado en el árbol DOM general. En las herramientas de depuración estos elementos del Light DOM quedan ocultos, pero se puede acceder a ellos sin problemas por medio de las propiedades y métodos del DOM, que han sido reescritos para este fin.

Este wrapper sobre las propiedades y métodos del DOM no es perfecto y, por ejemplo, el método getElementsByTagName() no funciona. Originalmente era el que estabamos utilizando en nuestro componente para acceder a los elementos del Light DOM. Como consecuencia hemos tenido que utilizar querySelectorAll(). No es un gran problema, y de hecho habitualmente utilizamos este método para acceder a los elementos, por lo que el cambio no es molesto.

const options = that.querySelectorAll ('option');

Otra limitación, en este caso algo más importante, es que querySelectorAll() no gestiona bien el selector :not(), no da ningún error, pero ignora esta cláusula, por lo que se obtiene un resultado incorrecto. Es un error interno del polyfill, pero de momento no tiene una solución fácil.

Lo primero que hemos tenido que hacer es saber si estamos trabajando con el Shadow DOM nativo o con el creado por el polyfill. De esta forma vamos a poder hacer una sencilla comprobación en cualquier circunstancia que tengamos que adaptar nuestro código de forma específica por estar utilizando el polyfill.

const HAS_NATIVE_SHADOW = Element
                            .prototype
                            .attachShadow
                            .toString ()
                            .indexOf ('[native code]') !== -1;

A partir de aquí podemos bifurcar nuestro código, para mantener el código original en los navegadores que utilizan el Shadow DOM nativo y aquellos que al utilizar el polyfill tienen alguna limitación y debemos ejecutar un código alternativo. Por ejemplo, en este método hemos tenido que realizar manualmente el filtro que la cláusula :not() debería haber realizado.

const selectAll  = this.shadowRoot.querySelector ('#selectAll');
const checkboxes = this.shadowRoot.querySelector ('#checkboxes');
let checked;
let visible;
if (HAS_NATIVE_SHADOW) {
  checked = checkboxes.querySelectorAll ('p:not(.hidden) input.option:checked').length;
  visible = checkboxes.querySelectorAll ('p:not(.hidden) input.option').length;
} else {
  checked = checkboxes
    .querySelectorAll ('p input.option:checked')
    .filter(o => ! o.parentNode.classList.contains('hidden'))
    .length;
  visible = checkboxes
    .querySelectorAll ('p input.option')
    .filter(o => ! o.parentNode.classList.contains('hidden'))
    .length;
}
selectAll.checked = checked === visible && checked !== 0;

Afortunadamente no son muchos los casos donde hemos tenido que aplicar esta duplicidad de código. Además de este con la cláusula :not() hemos tenido un problema con el atributo FOR de los LABEL, que por algún misterioso problema no funciona con el polyfill. Para resolverlo hemos tenido que incluir estas líneas adicionales:

dropdown.addEventListener ('click', (evt) => {
  if (!HAS_NATIVE_SHADOW && evt.target.hasAttribute('for')) {
    evt.target.parentElement.querySelector(`#${evt.target.getAttribute('for')}`).click();
    evt.cancelBubble = true;
    evt.preventDefault();

  }
  //...
}

MutationObserver y ShadyDOM

El mayor problema para que nuestro componente funcione con Microsoft Edge lo hemos encontrado con un bug identificado y conocido del polyfill y MutationObserver. Simplemente no funciona como debería, y en vez de observar el Light DOM, observa los cambios en el Shadow DOM. Aunque el equipo que da soporte a los polyfill conoce este problema y lo ha clasificado como de alto impacto, no tenemos una solución por su parte.

En la práctica implementar una versión alternativa del MutationObserver no es demasiado complicado, aunque pueda asustar, lo cierto es que son unas pocas líneas de código que tenemos que ejecutar cada pocos milisegundos comprobando los cambios que nos interesan dentro del Light DOM. Este es el código con la versión basada en MutationObserver y la basada en setTimeout().

if (HAS_NATIVE_SHADOW) {
  new MutationObserver (
    (mutations) => {
      let refresh = false;
      let update  = false;
      let change  = false;
      for (let mutation of mutations) {
        if (mutation.attributeName !== 'class') {
          refresh = true;
        }
        if (mutation.attributeName === 'selected') {
          change = true;
        }
        if (mutation.type === 'childList') {
          update = true;
        }
      }
      if (refresh) {
        this[ RENDER_REFRESH ] ();
      }
      if (update) {
        this.dispatchEvent (new Event ("update"));
      }
      if (change) {
        this.dispatchEvent (new Event ("change"));
      }
    }
  ).observe (
  this,
    {
      subtree         : true,
      attributes      : true,
      attributeFilter : [ 'selected' ],
      childList       : true,
      characterData   : true
    }
  );
} else {

  let prevOptions = [];
  const that      = this;
  (function observer () {
    const options = that.querySelectorAll ('option');
    if (options.length !== prevOptions.length) {
      that[ RENDER_REFRESH ] ();
      that.dispatchEvent (new Event ("update"));
      for (let n = 0; n < options.length; n++) {
        prevOptions[ n ] = {
          id        : options[ n ].id,
          value     : options[ n ].value,
          innerText : options[ n ].innerText,
          selected  : options[ n ].selected
        }
      }
    } else {
      let refresh = false;
      let update  = false;
      let change  = false;
      for (let n = 0; n < options.length; n++) {
        if (
          options[ n ].id !== prevOptions[ n ].id ||
          options[ n ].value !== prevOptions[ n ].value ||
          options[ n ].innerText !== prevOptions[ n ].innerText
        )
        {
          refresh                    = true;
          update                     = true;
          prevOptions[ n ].id        = options[ n ].id;
          prevOptions[ n ].value     = options[ n ].value;
          prevOptions[ n ].innerText = options[ n ].innerText;
        }
        if (options[ n ].selected !== prevOptions[ n ].selected) {
          prevOptions[ n ].selected = options[ n ].selected;
          refresh = true;
          change  = true;
        }
      }
      if (refresh) {
        that[ RENDER_REFRESH ] ();
      }
      if (update) {
        that.dispatchEvent (new Event ("update"));
      }
      if (change) {
        that.dispatchEvent (new Event ("change"));
      }
    }
    setTimeout (observer, 120);
  }) ();

}

Básicamente lo que hacemos es guardar la información que nos interesa en la variable prevOptions y comprobar si estos datos han cambiado desde la última vez que los consultamos. Para ello recorremos los elementos de tipo OPTION que tenemos en el Light DOM y revisamos sus propiedades y contenido. Si ha cambiado actuamos de forma similar a como lo hacemos en el caso con MutationObserver. Nosotros hemos decidido hacer esta comprobación cada 120 milisegundos, que para nuestro componente es más que suficiente. El retraso en la reacción ante un cambio en el Light DOM es muy pequeña, y aunque es realmente perceptible si uno se fija atentamente, apenas se nota.

Nuestros estilos y ShadyCSS

Lamentablemente el polyfill ShadyCSS sólo funciona con la etiqueta TEMPLATE y nosotros habíamos utilizado un Template String cargado directamente sobre this.shadowRoot.innerHTML. El cambio es menor y lo hemos podido hacer sin mayor dificultad.

Un segundo problema es que debemos aplicar explícitamente este polyfill, es decir, tenemos que llamar al método ShadyCSS.prepareTemplate() para que transforme nuestro código y modifique nuestras clases CSS a fin aislarlas, en la medida de lo posible, del entorno general. Es un poco engorroso, pero no debemos exagerar, es una línea de código adicional.

const template = document.createElement('template');
template.innerHTML = `
<link rel="stylesheet"
      href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<style>
  /* ... */
</style>
<div id="selection">
  <div id="selected"></div>
</div>
<div id="dropdown">
  <p class="group select-all">
    <input type="checkbox" id="selectAll" class="option">
    <label for="selectAll" class="mark"></label>
    <input type="search" id="search" placeholder="escribe aquí para filtrar">
  </p>
  <div id="checkboxes"></div>
</div>`;
  
window.ShadyCSS && ShadyCSS.prepareTemplate(template, 'todojs-multiselect');
this.shadowRoot.appendChild( template.content );

El resultado funciona bien, y aunque se pueden producir algunos conflictos, lo cierto es que en general aísla bastante bien el CSS si hemos tenido un poco de cuidado con nuestros selectores.

Futuro de Microsoft Edge

Lo más irónico de todo este esfuerzo, es que a finales de 2018 Microsoft anunció que las futuras versiones de Microsoft Edge se basarían en Chromium, es decir, que compartirían la base de Google Chrome. En agosto de 2019 se ha publicado una versión beta pública de la versión de Microsoft Edge basado en Chromium donde todo este esfuerzo ya no es necesario, ya que tiene el mismo soporte que Google Chrome a los componentes web. En los próximos meses se liberará esta versión al público en general.

Por ello hemos decidido mantener todos los cambios que hemos realizado en un fichero separado denominado todojs-multiselect-polyfill.js que se pueda utilizar en aquellos casos donde tenemos que dar soporte a las versiones de los navegadores que necesita del polyfill para utilizarse. En unos meses este fichero ya no tendrá utilidad si sólo nos centramos en navegadores modernos, pero de momento es lo que podemos utilizar en el caso de tener que utilizar los polyfills.

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.

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.

Template a fondo

Template a fondo

Hay dos formas estándar de crear contenido en un componente de forma flexible: la etiqueta template, que se considera como uno de los pilares de los Web Components y las template string de Javascript, que son una buena alternativa para generar el Shadow DOM con interpolación de datos.