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

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.