polymer: 4. shady DOM

Introducción al shadow dom especial de polymer

Vincent Van Gogh

archivado en: JavaScript / 9 enero, 2016 / taller:

Como ya sabemos, uno de los cuatro pilares de los web components es el shadow dom, una manera de encapsular el componente para que se relacione con el exterior de forma controlada y sin interferencias inesperadas. Polymer también trabaja con este tipo de dom local del componente, pero de una forma especial -denominada shady dom- que facilita el trabajo y la integración con los diferentes navegadores.

A diferencia de los web components nativos, la creación del dom local en polymer se produce de forma casi transparente, pues basta con declarar el consabido par de etiquetas <dom-module></dom-module>.

No es la única facilidad que nos aporta polymer, hay un montón más, pero para que se entienda lo que sigue vamos a ir haciendo un  web component que muestre la ficha de los personajes principales de la guerra de las galaxias.

De momento, preparamos un modelo muy sencillo, que más tarde mostrará los datos de forma dinámica y, como en esta entrada solo vamos a hacer un ejemplo, vamos a enlazar la librería polymer en el componente, que es como debe hacerse en la vida real, y no en la página receptora Algo así:

star-wars.html

<link rel="import" href="bower_components/polymer/polymer.html">

<dom-module id="star-wars">

<template>

<article id="main">

<h3 id="name">Luke Skywalker</h3>

<ul id="description">

<li><b>especie:</b> <span id="specie">humana</span></li>

<li><b>planeta natal:</b> <span id="homeworld">Tatooine</span></li>

<li><b>ocupación:</b> <span id="occupation">maestro jedy</span></li>

</ul>

</article>

</template>

<script>

Polymer({

is: "star-wars"

});

</script>

</dom-module>

Insertamos el componente en una página cualquiera...

index.html

<!DOCTYPE html>

<html lang="es">

<head>

<meta charset="UTF-8">

<title>Web Components</title>

<script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>

<link rel="import" href="star-wars.html">

</head>

<body>

<star-wars></star-wars>

</body>

</html>

Y, ale op, ahí está listo nuestro componente formidable.

wc-star-wars00

 

Localizar nodos por id: $ y $$

Como vimos, seleccionar nodos de un shadow dom nativo era bastante tedioso. Había que cachear el shadow.root y luego usar ese método tan farragoso que es getElementById. Con polymer es más sencillo, puesto que de forma automática mapea todos los nodos con id en this.$. Esto se aprecia bien si añadimos un ready con un console.log.

ready: function() {

console.log(this.$);

}

wc-star-wars01

Esto significa que podemos seleccionar cualquier nodo con id de una forma muy cómoda para hacer lo que sea. Por ejemplo, así pondríamos la especie de cada personaje en verde.

ready: function() {

this.$.specie.style.color = "#090";

}

Importante: este mapeo solo funciona con los nodos que se encuentran desde un principio en el template. Por ejemplo, si insertamos un nodo nuevo en el listado (y más adelante vemos qué significa eso de Polymer.dom()):

ready: function() {

var item = document.createElement('li');

item.innerHTML = "<b>notas:</b> <span id='notes'>su padre es Darth Vader</span>";

Polymer.dom(this.$.description).appendChild(item);

}

... y tratáramos de hacer la misma jugada de antes:

this.$.notes.style.color = "#090";

Nos saltaría un error -Uncaught TypeError: Cannot read property 'style' of undefined-, ya que el nodo no se encuentra de partida en el template, sino que ha sido creado dinámicamente (y esto mismo ocurre con los dom-repeat y los dom-if que veremos más adelante).

¡Dont Panic! en estos casos tenemos esta otra fórmula...

this.$$(selector)

la cual devuelve el primer ítem que se ajusta al selector. Así, en nuestro caso, bastaría hacer esto para poner ese nodo de color verde:

this.$$("#notes").style.color = "#090";

Polymer.dom(): seleccionar nodos

Polymer cuenta con su propia api para trabajar con el dom de un componente y es la que debemos utilizar cuando estemos trabajando con los nodos de un componente.

Los métodos de esta polyApi relacionados con la selección de nodos se pueden agrupar en dos grandes conjuntos en función del punto de partida:

  • Los que seleccionan un nodo padre (parent) y luego buscan nodos hijos dentro de él
  • Los que seleccionan un nodo (node) y luego buscan por los lados.

Entre los primeros están:

  • Polymer.dom(parent).querySelector(selector): el primer nodo que se ajusta al selector.
  • Polymer.dom(parent).querySelectorAll(selector): los nodos que se ajustan al selector.
  • Polymer.dom(parent).childNodes: todos los nodos hijos.
  • Polymer.dom(parent).children: todos los nodos hijos del tipo Element (es decir, ni comentarios, ni cadenas ni nada que no sean tags, para aclararnos).
  • Polymer.dom(node).firstChild: el primer nodo hijo.
  • Polymer.dom(node).lastChild: el último nodo hijo.
  • Polymer.dom(node).firstElementChild: el primer nodo hijo del tipo Element.
  • Polymer.dom(node).lastElementChild: el último nodo hijo del tipo Element.

Y entre los segundos:

  • Polymer.dom(node).parentNode: el padre del nodo seleccionado.
  • Polymer.dom(node).previousSibling: el primer hermano.
  • Polymer.dom(node).nextSibling: el hermano siguiente.

Como vemos, con estos métodos no hay nodo que no podamos cazar de una forma casi tan sencilla como ocurre en jQuery.

Un apunte más antes de cambiar de epígrafe. Los métodos que seleccionan varios nodos, a diferencia del api normal, no devuelven una NodeList, sino un array. Los matices al respecto son irrelevantes en la práctica real. Y, por cierto, recordemos que para manipular un nodo seleccionado así o bien lo hacemos por su posición en el array o bien recorriéndolo de alguna manera. Por ejemplo, en nuestro componente podríamos colorear el fondo de cada ítem de la ficha con un for:

ready: function() {

var allItems = Polymer.dom(this.$.description).children;

var i = 0;

var len = allItems.length;

for (; i<len; i++) {

allItems[i].style.background = "#369";

}

}

Polymer.dom(): manipular nodos

El api de polymer también tiene varios métodos habituales en la manipulación del dom.

  • Polymer.dom(node).textContent: modifica el contenido de texto de un nodo.
  • Polymer.dom(node).innerHTML: inserta un fragmento de html en un nodo.
  • Polymer.dom(parent).appendChild(node): cuelga un nodo de otro.
  • Polymer.dom(parent).insertBefore(node, beforeNode): inserta un nodo antes de otro nodo.
  • Polymer.dom(parent).removeChild(node): elimina un nodo.
  • Polymer.dom(node).setAttribute(attribute, value): modifica un atributo.
  • Polymer.dom(node).removeAttribute(attribute): elimina un atributo.
  • Polymer.dom(node).classList: como el classList normal de js, devuelve una DOMTokenList, a partir de la cual podemos añadir (add), quitar (remove), toglear (toglear) y comprobar una clase (contains).

Los métodos mencionados hasta aquí no difieren de los utilizados en javaScript normal, pero además polymer tiene otros tres para resolver necesidades específicas de la librería. Uno es Polymer.dom.flush(), que según explican en la documentación oficial:

«Insert, append, and remove operations are transacted lazily in certain cases for performance. In order to interrogate the DOM (for example, offsetHeight, getComputedStyle, etc.) immediately after one of these operations, call Polymer.dom.flush() first».

Es decir, en teoría este método sirve para asegurarse que el componente se ha terminado de renderizar de nuevo después de hacer una operación que modifica el árbol del dom (insertar y remover nodos), que como es asíncrona en polymer puede provocar errores de medición y demás... algo así como el apply de angular. Sin embargo, a decir verdad, en mis pruebas no he visto diferencia entre usarla o no o0.

Para ver los otros dos métodos, antes necesitamos saber cómo se relaciona en polymer un componente con los hijos que le llegan del mundo exterior.

Puntos de inserción

Como vimos en la entrada en la que expliqué el shadow dom, en la página en la que se usa un componente se pueden definir nodos que luego se recogen y se insertan mediante el tag <content>, el cual tiene un atributo denominado select en el que se utiliza un selector para indicar qué nodo del dom externo, el light dom, es el que va ahí. Así, por ejmplo, podríamos refactorizar nuestro componente star-wars para que los datos se puedan definir desde la página anfitriona.

Sacamos los datos fuera y, de paso, añadimos otra instancia del componente para ver cómo varía.

index.html

<star-wars>

<span id="name">Luke Skywalker</span>

<span id="specie">humana</span>

<span id="homeworld">Tatooine</span>

<span id="occupation">maestro jedy</span>

</star-wars>

<star-wars>

<span id="name">R2-D2</span>

<span id="specie">droide</span>

<span id="homeworld">Naboo</span>

<span id="occupation">droide astromecánico</span>

</star-wars>

Y cambiamos el componente añadiendo <content> donde corresponda.

star-wars.html

<dom-module id="star-wars">

<template>

<article id="main">

<h3><content select="#name"></content></h3>

<ul id="description">

<li><b>especie:</b> <content select="#specie"></content></li>

<li><b>planeta natal:</b> <content select="#homeworld"></content></li>

<li><b>ocupación:</b> <content select="#occupation"></content></li>

</ul>

</article>

</template>

<script>

Polymer({

is: "star-wars"

});

</script>

</dom-module>

Decía que había dos métodos para la relación del componente con estos nodos que llegan del mundo exterior. Uno es getDistributedNodes() y sirve para recuperar el nodo original, el que hay en el light dom que insertamos en un content. Para que se entienda, añadimos un id al <content> de la especie de tal manera que podamos seleccionarlo con comodidad...

<li><b>especie:</b> <content id="specie" select="#specie"></content>

Y ahora podemos hacer algo así para ver cómo funciona.

attached: function() {

/* Esto nos devuelve el span del light dom */

var specieInDomLight = Polymer.dom(this.$.specie).getDistributedNodes();

console.log("El contenido del nodo del dom exterior es un " + specieInDomLight[0].localName); // span

}

Por ver otro ejemplo, vamos a añadir un listado de imágenes relacionadas con cada personaje. Entre los nodos de la definición ponemos unas imágenes:

...

<div class="images" id="images">

<img src="luke-01.jpg">

<img src="luke-02.jpg">

<img src="luke-03.jpg">

<img src="luke-04.jpg">

</div>

...

Y luego en nuestro componente las insertamos en un content y, además, las contamos para mostrar en un <span> cuántas son.

...

<aside>

<h4>Imágenes <span id="imagesLength"></span></h4>

<content id="images" select=".images"></content>

</aside>

...

Polymer({

is: "star-wars",

attached: function() {

var images = Polymer.dom(this.$.images).getDistributedNodes();

if (images.length) {

document.getElementById("imagesLength").textContent = images[0].children.length;

} else {

/* Si no hay imágenes, nos cargamos el nodo */

var imageList = Polymer.dom(this.root).querySelector("aside");

Polymer.dom(this.$.main).removeChild(imageList);

}

}

});

st-fin

Me quedaría por hablar de getDestinationPoints(), que nos devuelve los puntos donde se ha insertado algo, pero lo cierto es que no le veo mucha utilidad y es bastante confuso, así que de momento lo paso por alto. Dejo igualmente para otro momento el cacao de los nodos efectivos, que ni siquiera está bien explicado en la documentación oficial, y termino ya esta toxo-entrada con una reflexión.

¿Crónica de un desastre anunciado?

Primera premisa: Por razones que desconozco, quizás porque los programadores de google desconocen cuál es la realidad de los mercados normales, los productos de esta compañía para el desarrollo web cambian demasiado de una versión a otra. Ha pasado con angular, cuya versión 2 no es compatible con la 1, y con polymer, que en el salto a la versión 1 ha dejado de ser compatible con la 0.5 que se estaba manejando. Esto significa que las empresas que han invertido en estas tecnologías pueden tener que volver a desembolsar cantidades espantosas si no quieren que sus productos estén soportados con frames o librerías deprecated, con todo el riesgo que supone para el mantenimiento y la escalabilidad.

En fin, Este tema es muy complejo y analizarlo en profundidad me llevaría por derroteros ajenos a polymer, por lo que baste con decir que no me fío de google, pues considero que los productos web deben tener una perdurabilidad que compense la inversión (el roi famoso del que hablan los marketinianos), ya que no está el mundo para ir tirando el dinero.

Segunda premisa: Uno de los aspectos más interesantes de los web components es que se basan en un estándar y esto significa que, en teoría, todos los navegadores, todos los dispositivos, tiene una referencia para hacer las cosas bien y, por lo tanto, que las web y aplicaciones web sean universales, lo cual es formidable. De hecho, volver a un Internet monopolizado por una compañía, donde las cosas solo se vean bien en chrome y android, me parece terrible. Cabe preguntarse entonces sobre las razones por las que google ha decidido implementar su propio shadow dom al margen del estándar.

Scott Milles, trabajador de google, lo explica en un artículo titulado What is shady DOM. Segun Milles, el problema principal es que el polyfill para que el shadow dom funcione en todos los navegadores es muy farragoso y ralentiza en gran medida todo el tinglado.

«The Shadow DOM Polyfill actually attempts this task, but there are costs:

  • It’s a lot of code.
  • It’s slow to indirect all the DOM API.
  • Structures like NodeList can simply not be emulated.
  • There are certain accessors that cannot be overwritten (for example, window.document,window.document.body).
  • The polyfill returns objects that are not actually Nodes, but Node proxies, which can be very confusing.

»Many projects simply cannot afford the Shadow DOM Polyfill, for the reasons given above. In particular, the performance costs on platforms like mobile Safari are nearly intolerable».

Y ahora si metemos todo en el caldero podemos hacer la siguiente reflexión. Los navegadores evolucionan y van mejorando, por lo que es cuestión de tiempo que no necesiten polyfill alguno para implementar el shadow dom; lo cual solo le dejará dos alternativas al equipo de polymer:

  • Seguir con su shady dom al margen del estándar, lo cual en principio no mola nada.
  • En la línea que caracteriza a google, sacar una versión no compatible con la actual en la que se vuelva al shadow dom, lo que dejará deprecated todos los componentes realizados hasta la fecha.

Quedaría una tercera solución, la chula, que sería sacar una versión con shadow dom, pero compatible con la actual; pero esta línea de actuación, insisto, no es la que caracteriza a google. Y con esta incertidumbre cierro esta entrada.

 Adenda

Le podemos decir a polymer que use el shadow dom en lugar del shady-dom indicándolo antes de cargar un componente.

<script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>

<script>

window.Polymer = window.Polymer || {};

window.Polymer.dom = 'shadow';

</script>

<link rel="import" href="star-wars.html">

En el momento de escribir estas líneas no sé si es mejor trabajar así para tener que refactorizar menos código más adelante o no.

|| Tags: , ,

valoración de los lectores sobre polymer: 4. shady DOM

  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración negativa
  • 4.3 sobre 5 (6 votos)

¿Te ha parecido útil o interesante esta entrada?
dormido, valoración 1 nadapensativo, valoración 2 un poco sonrisa, valoración 3 a medias guiño, valoración 4 bastante aplauso, valoración 5 mucho

Tú opinión es muy importante, gracias por compartirla!

Los comentarios están cerrados.