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
HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)
En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.
En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.
Observables en Javascript con Proxies
En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.
Aplicaciones JAMStack, SEO friendly y escalables con NextJS
En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.
Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera
En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.