Seleccionar página
Es probable que el DOM se nos haya ido de las manos: creamos y gestionamos miles de elementos anidados en árboles interminables, con identificadores y estilos que terminan colisionando unos con otros. Es en este contexto donde el Shadow DOM nos ofrece una solución muy interesante, con una forma muy efectiva de encapsular los elementos del DOM y el CSS.

Este Shadow DOM, DOM en la sombra, o DOM oculto, es una solución que se puede utilizar tanto con elementos predefinidos del HTML, como con Web Components definidos por nosotros. Casi siempre lo utilizaremos asociado a los componentes web, pero es importante comprender que se puede utilizar de forma independiente a ellos, ya que es una característica general del DOM y no sólo una solución de componentes web.

Con el objetivo de que podamos comprender su funcionamiento y conocerlo a fondo, os proponemos dar un repaso en profundidad al Shadow DOM. Empezaremos desde lo más básico, comprendiendo cómo funciona y para qué sirve aplicado a elementos estándar del HTML, para ir poco a poco profundizando en sus diferentes características y algunos de sus secretos junto a los componentes web.

¿Cómo se crea un Shadow DOM y para qué sirve?

Crear un Shadow DOM

Muchos de los elementos predefinidos del HTML tienen la capacidad de tener un árbol DOM aislado, separado del DOM principal del documento. Para crearlo basta con utilizar el método .attachShadow() sobre el elemento (más adelante veremos cómo funciona en detalle). Es verdaderamente sencillo. Al invocar ese método se devuelve un shadowRoot, que es el punto de partida de un DOM aislado. Este DOM no es accesible directamente desde el DOM general y se gestiona localmente al elemento donde se ha creado.

Para comprender mejor su comportamiento hemos creado este pequeño ejemplo, que se puede ejecutar o modificar para que puedas comprender mejor cómo funciona.

<p>Test</p>
<button id="find">document.querySelectorAll('p')</button>
<button id="attach">AttachShadow</button>
<div id="test"></div>
<pre></pre>
<script>
  const attach = document.querySelector ('#attach');
  const div    = document.querySelector ('#test');
  const find   = document.querySelector ('#find');
  const result = document.querySelector ('pre');

  attach.addEventListener ('click', function () {
    div.attachShadow ({mode : 'open'});
    div.shadowRoot.innerHTML = '<p>DIV shadow content (&lt;p&gt;)</p>';
    result.innerHTML         = '';
  });

  find.addEventListener ('click', function () {
    const element    = document.querySelectorAll ('p');
    result.innerHTML = 'found ' + element.length + ' elements';
  });
</script>

Aunque hemos creado un nuevo elemento P, este ha sido insertado dentro del Shadow DOM del elemento DIV, por lo que una instrucción como document.querySelector('p') no puede encontrarla.

Elementos que soportan attachShadow

Como hemos comentado, el Shadow DOM se puede aplicar a muchos elementos estándar del HTML, pero no a todos. Podemos añadir un Shadow DOM a los siguientes elementos estándar del HTML:

article, aside, blockquote, body, div, footer, h1, h2, h3, h4, h5, h6, header, main, nav, p, section y span.

Si lo pensamos un poco, hay otros elementos, como un img o un input que no pueden aceptar un Shadow DOM, ya no sabrían que hacer con él, por lo que sólo podemos añadirlo a uno de los componentes enumerados en esta lista.

Como veremos en detalle más adelante, HTMLElement y todos los Web Components autónomos que heredan de él pueden tener un Shadow DOM, permitiendo crear un contexto aislado y sin colisiones para los elementos del DOM y el CSS de nuestros componentes.

Aislamiento del DOM

La gran aportación del Shadow DOM es el aislamiento que tiene este DOM, que actúa sólo localmente en elemento donde ha sido definido. Por ejemplo, podemos repetir en el DOM oculto un id o un atributo name que ya existan en el DOM general, sin producir ningún tipo de conflicto.

En general el HTML gestiona los conflictos de forma silenciosa, pero esto no quiere decir que no los haya cuando, por ejemplo, se encuentra atributos como los identificadores repetidos, los problemas se hacen visibles cuando queremos encontrar uno de estos elementos con métodos del tipo document.querySelector().

En el siguiente ejemplo, cuando pulsamos el botón y se ejecuta querySelectorAll ('#myid'); solo encontramos un elemento con el id myid, ya que el segundo ha sido creado dentro del contexto Shadow DOM del elemento div.

<button id="find">document.querySelectorAll('#myid')</button>
<p id="myid">global context (&lt;p id="myid"&gt;)</p>
<div id="test"></div>
<pre id="result"></pre>
<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});
  div.shadowRoot.innerHTML = '<p id="myid">shadow context (&lt;p id="myid"&gt;)</p>';

  const find   = document.querySelector ('#find');
  const result = document.querySelector ('#result');
  find.addEventListener ('click', function () {
    const elements   = document.querySelectorAll ('#myid');
    result.innerHTML = 'found ' + elements.length + ' elements';
  });
</script>

Aunque en ocasiones podamos adentrarnos en el Shadow DOM (lo vamos a ver un poco más adelante), lo cierto es que este aislamiento es muy útil para poder combinar diferentes componentes sin que nos tengamos que preocupar sobre las colisiones que sus elementos internos puedan producir, ya que cada uno gestiona y mantiene su propio DOM aislado.

Aislamiento del CSS

De igual forma que está aislado el DOM, en el Shadow DOM también estamos aislados en cuanto a los estilos CSS: los estilos definidos globalmente no nos afectan y los estilos que definamos en nuestro DOM no afectarán fuera de nuestro contexto.

En el siguiente ejemplo podemos ver como los estilos globales para el elemento button no se aplican para el elemento creado dentro del Shadow DOM y como los estilos creados dentro del Shadow DOM no afectan al botón creado en el contexto global. El ejemplo que se puede ejecutar directamente en repl.it.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS and Shadow DOM</title>
  <style>
    button {
      background-color: #00b000;
      color: #ffffff;
      font-family: monospace;
      font-size: 14px;
      line-height: 28px;
    }
  </style>
</head>
<body>

<button>Global context</button>

<div id="test"></div>

<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});

  const style      = document.createElement ('style');
  style.innerHTML  =
    'button {' +
    '  color      : #ff0000;' +
    '  font-size  : 14px;' +
    '  line-height: 28px;' +
    '}';

  const button     = document.createElement ('button');
  button.innerHTML = 'shadow context';

  div.shadowRoot.appendChild (style);
  div.shadowRoot.appendChild (button);
</script>
</body>

Aunque este aislamiento pueda complicarnos un poco la vida a la hora de crear un estilo uniforme en todos los componentes, lo cierto es que poder definir CSS con un contexto de aplicación acotado al Shadow DOM da un gran nivel de granularidad a nuestros estilos, pudiendo centrarnos en la visualización de un componente, sin temer las colisiones en cuanto a nombres de clases o identificadores y sus estilos.

El Shadow DOM no aísla el Javascript

Lamentablemente podemos encontrar en Internet algunas referencias que dicen que el Shadow DOM aísla el Javascript. Esto último no es cierto, por sí mismo no aísla nuestros programas. Si inyectamos un script en el Shadow DOM de un elemento, este se ejecuta en el contexto global del navegador y si, por ejemplo, crea una variable global, está residirá en el objeto window, que es el objeto global de los navegadores.

<div id="test"></div>
<pre></pre>
<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});

  const script     = document.createElement ('script');
  script.innerHTML = 'var a = 10';
  div.shadowRoot.appendChild (script);

  const result = document.querySelector('pre');
  result.innerHTML = 'window.a ===' + window.a;
</script>
 

En este ejemplo, la variable creada dentro del script que hemos insertado en el Shadow DOM se ha creado de forma global, por lo que no se produce un error obtener el valor de window.a.

Soporte y polyfill

El Shadow DOM está disponible en:

Chrome 53, Opera 40 y Safari 10

Lamentablemente no está disponible en

Internet Explorer y Edge (se espera esté disponible en una próxima versión).

Existen dos polyfills para navegadores que no tienen soporte a Shadow DOM. Realmente no pueden hacer lo mismo que conseguimos con un DOM aislado, pero al menos evitan algunos de los problemas más importantes. Se denominan:

  • Shady DOM imita el comportamiento de Shadow DOM.
  • Shady CSS que simula algunas de las capacidades del CSS

Se instalan con:

npm install @webcomponents/shadydom
npm install @webcomponents/shadycss

Ya advertimos que los resultados de estos polyfill dejan mucho que desear, pero es que conseguir el aislamiento del Shadow DOM no es tarea fácil y prácticamente imposible desde un código Javascript que intenta reemplazar una característica de este tipo de la que carece determinado navegador.

Método .attachShadow()

Ahora que hemos dado un primer vistazo al Shadow DOM y sabemos para que sirve, .attachShadow(), ya que tiene algunas características que debemos tener en cuenta.

Seguramente se habrán percatado de que cuando llamamos al método .attachShadow() siempre debemos pasar un objeto como parámetro. Este objeto es obligatorio y si no lo incluimos se producirá un error. Actualmente este objeto de configuración sólo tiene definido una propiedad (ya hay alguna propuesta para ampliarlo con alguna otra información), esta propiedad es mode y establece si se puede acceder al Shadow DOM que estamos creando por medio de la propiedad .shadowRoot.

La segunda característica que debemos tener en cuenta es que el método .attachShadow devuelve una referencia a la raíz del Shadow DOM que acaba de crear, tanto si se ha creado en modo open como closed y podemos utilizar esta referencia para insertar o modificar elementos en el DOM aislado.

Por lo tanto, tenemos las siguientes opciones:

  • si al método .attachShadow() le pasamos el objeto {mode:'open'}, entonces la propiedad .shadowRoot contendrá la raíz de nuestro DOM aislado, además de haber sido devuelto en la llamada al método.
  • si al método .attachShadow() le pasamos el objeto {mode:'closed'}, entonces la propiedad .shadowRoot contendrá null, y sólo obtendremos una referencia a este DOM como retorno de la llamada al método.

Vamos a intentar mostrar la diferencia en este ejemplo con dos div, uno en el que crea un Shadow DOM en modo open y otro en el que se crea en modo closed. Si intentamos modificar el contenido con .shadowRoot veremos que sólo es posible en el modo abierto:

<div id="open"></div>
<div id="closed"></div>
<script>
  const divOpen            = document.querySelector ('#open');
  const shadowRootOpen     = divOpen.attachShadow ({mode : 'open'});
  shadowRootOpen.innerHTML = '<h1 part="h1">open</h1>';

  const divClosed            = document.querySelector ('#closed');
  const shadowRootClosed     = divClosed.attachShadow ({mode : 'closed'});
  shadowRootClosed.innerHTML = '<h1 part="h1">closed</h1>';

  if (divOpen.shadowRoot !== null) {
    divOpen.shadowRoot.querySelector ('h1').innerHTML = 'open means accesible';
  }
  if (divClosed.shadowRoot !== null) {
    divOpen.shadowRoot.querySelector ('h1').innerHTML = 'closed means unaccesible';
  }
</script>

Se podrá observar que el elemento con un Shadow DOM creado en modo closed no ha sido modificado ya que la propiedad .shadowRoot contiene null.

¿Es realmente privado el Shadow DOM?

En primer lugar, no hemos dicho en ningún momento que sea privado», hemos dicho que «encapsula» y «aísla», pero no hemos dicho en ningún momento que aporte privacidad a los elementos del DOM que están en la parte Shadow. Probablemente las características de este DOM nos den una sensación de privacidad, pero no es así.

Como hemos podido observar, los Shadow DOM creados en modo open pueden ser consultados y manipulados de forma completa por medio de la propiedad .shadowRoot, por lo que, aunque están aislados, no están realmente ocultos, sólo ensobrecidos, es decir, es algo más complicado acceder a su contenido, pero podemos llegar a actuar sobre ellos sin limitaciones.

En el modo closed la cosa es un poco más cercana a la privacidad, ya que debemos tener acceso al valor devuelto por el método .attachShadow() y este puede estar encapsulado en el alcance del programa o dentro de una clase. De esta forma podemos dar algo más de privacidad y limitar el acceso al contenido de DOM.

Shadow DOM open and closedNo obstante, si realmente queremos tener el contenido de nuestro Shadow DOM al margen de miradas curiosas, tenemos una mala noticia, las herramientas de desarrollo de los navegadores dan plena visibilidad al contenido del DOM, aunque se haya definido como closed, por lo que no podemos utilizar este mecanismo para ocultar cosas que realmente deban mantenerse en secreto.

Hay una cierta tendencia a crear los Shadow DOM en modo open, en parte por la comodidad que supone disponer de una propiedad para el acceso al DOM interno del componente, en parte por recomendaciones que se han hecho por parte de Google para que mantengamos este modo en nuestros componentes. Lo curioso es que algunos elementos estándar del HTML tienen su propio Shadow DOM, por ejemplo, la etiqueta video y lo definen en modo closed. Que cada uno decida cómo quiere utilizar esta funcionalidad.

El shadowRoot

Ya sea por medio de la propiedad .shadowRoot o por la referencia devuelta por el método .attachShadow(), la raíz de nuestro DOM en la sombra es fundamental para las operaciones para crear o modificar el contenido de nuestro componente. Es a partir de este elemento del que colgaremos todo nuestro árbol de elementos y por lo tanto es importante conocer sus características.

En primer lugar tenemos que saber que es como cualquier otro elemento del DOM, y podemos consultarlo con .querySelector(), añadir nuevos elementos con .appendChild(), ver o modificar su contenido completo con .innerHTML, recórrelo con .children, .firstElementChild, .lastElementChild, .previousElementSibling, .nextElementSibling, etc.

No hay especiales limitaciones en su funcionamiento, aunque si tiene algunas características propias que vamos a repasar a continuación…

.host

Tenemos que tener en cuenta que el shadowRoot funciona como un elemento raíz, es decir, las propiedades .parentElement y .parentNode contienen null. No podemos navegar hacia arriba en árbol por estas propiedades.

Si lo necesitamos, para acceder al contenedor de shadowRoot tenemos que utilizar la propiedad .host, que apunta a elemento donde se ha invocado el método .attachShadow. Esta es una propiedad que sólo tiene los DOM en la sombra.

A partir de .host podemos navegar por el DOM de nivel superior, pudiendo hacer consultas y modificaciones sin restricciones.

<div id="test"></div>
<script>
  const div        = document.querySelector ('#test');
  const shadowRoot = div.attachShadow ({mode : 'open'});
  console.assert (shadowRoot.parentElement === null);
  console.assert (shadowRoot.parentNode === null);
  shadowRoot.host.parentElement.style.backgroundColor = 'red';
</script>
 

Es este ejemplo podemos ver cómo utilizar la propiedad .host para acceder a nuestro contenedor y, a partir de ahí, acceder al resto del DOM.

.mode

Si queremos consultar si un shadowRoot ha sido creado en modo cerrado o abierto podemos consultar la propiedad . mode que contiene los textos open o closed. Es una propiedad de sólo lectura y no podemos modificarla una vez que ha sido creado el árbol DOM de nuestro componente.

<div id="test"></div>
<script>
  const div        = document.querySelector ('#test');
  const shadowRoot = div.attachShadow ({mode : 'closed'});
  console.assert (shadowRoot.mode === 'closed');
</script>

.activeElement

Esta propiedad la comparte shadowRoot con document, ya que ambos son elementos raíces de un árbol DOM. La propiedad .activeElement contiene una referencia al elemento hijo que tiene en ese momento el foco. Si no hay ningún elemento seleccionado, entonces contiene null.

<div id="test"></div>
<script>
  const div        = document.querySelector ('#test');
  const shadowRoot = div.attachShadow ({mode : 'closed'});
  const check = document.createElement('button');
  check.innerHTML = 'check';
  check.addEventListener('click', function() {
    shadowRoot.activeElement.style.backgroundColor = 'red';
  });
  shadowRoot.appendChild(check);
</script>

El ejemplo muestra como se cambia el color de fondo del elemento que tiene el foco cuando se pulsa el botón, es decir, el mismo.

.styleSheets

Esta propiedad también la comparte shadowRoot con document, ya que ambos pueden tener en su árbol etiquetas style. La propiedad .styleSheets contiene una matriz con todas las hojas de estilo que se han creado en el DOM.

<div id="test"></div>
<pre></pre>
<script>
  const div        = document.querySelector ('#test');
  const shadowRoot = div.attachShadow ({mode : 'closed'});
  const style      = document.createElement('style');
  style.innerHTML  =
    'h1 {' +
    '  color    : red;' +
    '  font-size: 2em;' +
    '}';
  shadowRoot.appendChild(style);
  const h1     = document.createElement('h1');
  h1.innerHTML = 'Header 1';
  shadowRoot.appendChild(h1);

  const result = document.querySelector('pre');
  result.innerHTML = shadowRoot.styleSheets.length + ' style sheet found';
</script>

En este ejemplo se muestra el número de hojas de estilo que están definidos en nuestro DOM privado.

Otros métodos

Otros métodos que shadowRoot comparte con document y que recogemos aquí de forma abreviada para no extendernos mucho son:

Shadow DOM y el CSS

Tal y como comentamos al inicio, una de las grandes virtudes del Shadow DOM es el aislamiento que produce en cuanto al CSS, que se traduce en que los estilos definidos fuera del DOM en la sombra no afectan dentro y los creados dentro no afectan fuera, por lo que se evitan conflictos, aunque -como consecuencia- tampoco se pueden aprovechar directamente los estilos de unos y otros.

Esto es especialmente importante, ya que si definimos un estilo global, por ejemplo, para los botones de nuestra aplicación o página web, este estilo no se aplicará a los botones creados dentro de nuestro DOM aislado. Esto complica un poco como mantener un estilo homogéneo entre los distintos componentes, pero no es imposible. Vamos a profundizar un poco más en cómo manejar los estilos CSS en nuestros Shadow DOM de forma eficiente y controlada.

Insertar estilos en el shadowRoot

En nuestros shadowRoot podemos añadir estilos de dos formas diferentes:

  • añadiendo una etiqueta style e incluyendo el CSS como parte del .innerHTML, incluyendo -si lo necesitamos- cláusulas @import,
  • añadiendo una etiqueta link a la que configuramos sus diferentes atributos para que apunte a una hoja de estilos.

En este ejemplo estamos creando enlazando con la hoja de estilos de bulma CSS ubicada en una CDN y creando un estilo propio dentro del shadowRoot. Ambos estilos sólo se aplican al botón que hay dentro de DOM en la sombra, y no se aplican al botón creado en el contexto global.

<button class="button">Global context</button>
<div id="test"></div>
<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});

  const link = document.createElement ('link');
  link.setAttribute('rel', 'stylesheet');
  link.setAttribute('type', 'text/css');
  link.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.css');

  const style = document.createElement ('style');
  style.innerHTML =
    '.button {' +
    '  background-color: lightgreen' +
    '}';

  const button     = document.createElement ('button');
  button.classList.add('button');
  button.innerHTML = 'shadow context';

  div.shadowRoot.appendChild (link);
  div.shadowRoot.appendChild (style);
  div.shadowRoot.appendChild (button);
</script>

En la práctica podemos manipular estos estilos tanto como nos permitan los navegadores. Por ejemplo, en las últimas versiones de Chrome y Firefox empezamos a tener acceso al CSS Object Model que nos facilita la manipulación de los estilos desde Javascript. Estas son capacidades generales de los navegadores y el CSS que soportan.

Adicionalmente a las capacidades del CSS de cada navegador, los estilos incluidos en un Shadow DOM disponen de algunas capacidades específicas para gestionar algunas de sus características. Vamos a ver algunas de ellas.

:host

En los estilos contenidos en el shadowRoot podemos hacer referencia a nuestro contenedor por medio de la pseudo clase :host. Este selector se refiere al elemento que contiene este Shadow DOM y nos permite configurar su estilo. Debemos tener en cuenta que el contenedor en el que hemos creado el shadowRoot no forma parte de Shadow DOM y es parte del DOM de nivel superior. Con :host podemos hacer referencia a este elemento por encima del DOM en que nos encontramos.

<div id="test"></div>
<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});

  const style = document.createElement ('style');
  style.innerHTML =
    ':host {' +
    '  display         : table-cell;' +
    '  text-align      : center;' +
    '  height          : 5em;' +
    '  width           : 10em;' +
    '  vertical-align  : middle;' +
    '  background-color: lightgreen;' +
    '  border          : 1px solid darkgreen;' +
    '}';

  const button     = document.createElement ('button');
  button.classList.add('button');
  button.innerHTML = 'shadow context';

  div.shadowRoot.appendChild (style);
  div.shadowRoot.appendChild (button);
</script>

En este ejemplo estamos dando estilo al elemento div donde hemos creado el shadowRoot por medio de :host.

:host()

Aunque se parece mucho al selector anterior, se comporta de una forma algo diferente cuando añadimos los paréntesis e incluimos un selector. :host(selector) sólo se aplica si el selector que se pasa entre paréntesis es cierto para el elemento contenedor. Si, por ejemplo, se incluye una determinada clase, el estilo se aplicará si tiene asignada esa clase, si se incluye un atributo como selector, entonces se aplicará si tiene ese atributo.

Este selector sólo se aplica al componente :host como tal y no a sus padres o su contexto, es decir, la clase, el identificador, el atributo que pongamos como selector, debe estar incluido dentro del componente que alberga el shadowRoot.

<div id="test" class="alert" hidden></div>

<p>
  <button id="show">show/hide element</button>
  <button id="change">change class .alert</button>
</p>

<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});

  const style     = document.createElement ('style');
  style.innerHTML =
    ':host {' +
    '  display         : table-cell;' +
    '  text-align      : center;' +
    '  height          : 5em;' +
    '  width           : 10em;' +
    '  vertical-align  : middle;' +
    '  background-color: lightgreen;' +
    '  border          : 1px solid darkgreen;' +
    '}' +
    ':host([hidden]) { ' +
    '  display: none ' +
    '}' +
    ':host(.alert) button {' +
    '  color: red;' +
    '}';

  const button = document.createElement ('button');
  button.classList.add ('button');
  button.innerHTML = 'shadow context';

  div.shadowRoot.appendChild (style);
  div.shadowRoot.appendChild (button);

  document.querySelector ('#show').addEventListener ('click', function () {
    div.hasAttribute ('hidden') ?
      div.removeAttribute ('hidden') :
      div.setAttribute ('hidden', '')
  });

  document.querySelector ('#change').addEventListener ('click', function () {
    div.classList.toggle ('alert');
  });
</script>

En este ejemplo podemos ver como dependiendo de una clase o de un atributo el contenedor de nuestro shadowRoot cambia su estilo por medio de :host(selector).

Esta es una primera aproximación a cómo podemos configurar el estilo de un componente por medio de clases y atributos externos al mismo, conteniéndose dentro de él las clases que materializan esta configuración.

:host-context()

Aunque se parece al selector que acabamos de ver, :host-context(selector) permite configurar en estilo de nuestro componente cuando el selector que hemos indicado coincida con alguno de los elementos superiores de árbol DOM, es decir, podemos configurar un estilo cuando nuestro componente esté dentro de un elemento que tenga una determinada clase, o tenga un determinado atributo, etc.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS and Shadow DOM (:host-context(selector))</title>
  <style>
    body.dark {
      background-color: black;
      color           : white;
    }
  </style>
</head>
<body class="dark">
<div id="test" class="alert"></div>
<p>
  <button id="change">change BODY class .dart to .light</button>
</p>
<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});

  const style     = document.createElement ('style');
  style.innerHTML =
    ':host-context(.dark) button {' +
    '  background-color: #555;' +
    '  color           : #C0C0C0;' +
    '  border          : 1px solid grey;' +
    '}' +
    ':host-context(.light) button {' +
    '  background-color: #F0F0F0;' +
    '  color           : #0C0C0C;' +
    '  border          : 1px solid back;' +
    '}';

  const button = document.createElement ('button');
  button.classList.add ('button');
  button.innerHTML = 'shadow context';

  div.shadowRoot.appendChild (style);
  div.shadowRoot.appendChild (button);

  document.querySelector ('#change').addEventListener ('click', function () {
    document.body.classList.toggle ('dark');
    document.body.classList.toggle ('light');
  });
</script>
</body>
</html>
 

En este ejemplo se cambia el estilo interno del shadowRoot cuando cambia la clase que se ha asociado a body.

Custom property de CSS

Una buena noticia es que las propiedades personalizadas del CSS, también conocidas como variables CSS, sí traspasan el Shadow DOM y aquellas que han sido definidas a nivel superior, son accesibles y utilizables dentro de los estilos definidos en el shadowRoot. Esto permite gestionar los valores que aplicamos a los diferentes estilos de forma globa.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS and Shadow DOM (var())</title>
  <style>
    body {
      --yes-color           : red;
      --yes-background-color: yellow;
      --no-color            : yellow;
      --no-background-color : red;
    }
  </style>
</head>
<body>
<div id="test" class="alert"></div>
<script>
  const div = document.querySelector ('#test');
  div.attachShadow ({mode : 'open'});
  div.shadowRoot.innerHTML =
    '<style>' +
    '.yes {' +
    '  background-color: var(--yes-background-color);' +
    '  color           : var(--yes-color);' +
    '}' +
    '.no {' +
    '  background-color: var(--no-background-color);' +
    '  color           : var(--no-color);' +
    '}' +
    '</style>' +
    '<button class="yes">Yes</button> ' +
    '<button class="no">No</button>';
</script>
</body>
</html>

En este ejemplo hemos definido unas propiedades personalizadas en la cabecera de la página y las estamos usando dentro de la etiqueta style que hemos incluido dentro del Shadow DOM.

Hay que tener en cuenta que la utilización de estas propiedades personalizadas dentro de ShadowRoot producen un acoplamiento y, por lo tanto, una dependencia, entre el componente y el contexto.

Aunque no resuelve este problema de acoplamiento, para evitar que la no existencia de una determinada variable pueda producir un problema en nuestros estilos, se recomienda utilizar valores por defecto en las variables, por ejemplo:

const div = document.querySelector ('#test');
div.attachShadow ({mode : 'open'});
div.shadowRoot.innerHTML =
  '<style>' +
  '.yes {' +
  '  background-color: var(--yes-background-color, #000000);' +
  '  color           : var(--yes-color, #FFFFFF);' +
  '}' +
  '.no {' +
  '  background-color: var(--no-background-color, #FFFFFF);' +
  '  color           : var(--no-color, #000000);' +
  '}' +
  '</style>' +
  '<button class="yes">Yes</button> ' +
  '<button class="no">No</button>';

Lamentablemente no todos los navegadores tienen soporte a esta funcionalidad del CSS, pero su uso está bastante extendido en las versiones más modernas, por lo que es un recurso a nuestro alcance para gestionar estilos homogéneos.

::part()

Esta nuevo pseudo elemento del CSS, ::part() se ha propuesto hace unos meses como un mecanismo flexible para la personalización los estilos de un Shadow DOM, manteniendo el control de parte del desarrollador del componente. Aunque es reciente, ya está disponible en Chrome 70 (octubre de 2018) y podemos empezar a familiarizarnos con él.

Para comprender su funcionamiento tenemos que empezar definiendo que partes de nuestro Shadow DOM que queremos se puedan personalizar externamente y añadirles el atributo part con un nombre identificador. De esta forma estamos definiendo las partes estilables de nuestro componente y asignándoles un nombre.

const div = document.querySelector ('#test');
div.attachShadow ({mode : 'open'});
div.shadowRoot.innerHTML =
  '<button part="button-yes">Yes</button> ' +
  '<button part="button-no">No</button>';

Para dar estilos a estar partes que se han definido como estilables sólo tenemos que, desde el DOM externo al componente, crear un estilo con el pseudo elemento ::part(identificador).

<style>
  div#test::part(button-yes) {
    color           : red;
    background-color: yellow;
  }
  div#test::part(button-no) {
    color           : yellow;
    background-color: red;
  }
</style>

Con una versión moderna de Chrome se puede ejecutar y personalizar ejemplo y comprobar cómo funciona en la práctica.

:shadow and /deep/

Es posible encontrar en Internet bastantes referencias a estos selectores que funcionaron en versiones iniciales de Shadow DOM pero que ya han sido declaradas obsoletas (deprecated). De hecho, se han retirado de los navegadores que les daban soporte, por ejemplo, ya no están disponibles desde Chrome 60. Por ello, no se debe utilizar :shadow o /deep/ en nuestras hojas de estilo, si por alguna casualidad funciona, lo dejará de hacer en breve.

Shadow DOM en un Web Component

Quizás alguno estaba ya impaciente por ver cómo utilizar el Shadow DOM en un Web Component que heredan de HTMLElement. Hasta ahora nos hemos centrado en su utilización con elementos del HTML, ya que son una buena forma de comprender cómo funcionan. Ahora vamos a ver cómo aplicar todo lo que hemos visto en un Web Component y crear un Shadow DOM para evitar colisiones entre en el DOM y el CSS de nuestros componentes.

attachShadow() en el constructor

El Shadow DOM lo crearemos en el constructor aplicado al objeto this que corresponde al custom element. Cualquier otro callback, de los que están disponibles en HTMLElement, puede ser ejecutado más de una vez y el método attachShadow() no puede invocarse dos veces sobre el mismo elemento. Es bastante sencillo:

<my-component></my-component>
<script>
  class MyComponent extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = '<h1>Custom Element with Shadow DOM</h1>';
    }
  }
  customElements.define('my-component', MyComponent);
</script>

En este ejemplo hemos añadido un Shadow DOM a nuestro componente my-component y hemos añadido contenido dentro de él.

Modo closed

Si creamos un Shadow DOM en modo cerrado, deberemos guardar el valor devuelto en alguna propiedad o en algún elemento en nuestro alcance si queremos que otros métodos puedan hacer uso de él, ya que this.shadowRoot será null. Si por el contrario, está creado en modo abierto, entonces podemos acceder sin problemas con this.shadowRoot.

Para guardar esta referencia al shadowRoot creado en modo cerrado podemos hacer uso de diferentes técnicas, como almacenarlo en una propiedad definida con un Symbol o utilizar un WeakMap. En este ejemplo vamos a utilizar esta última opción.

<my-component num="0"></my-component>
<script>
  {
    const priv = new WeakMap ();

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

      constructor () {
        super ();
        const shadowRoot = this.attachShadow ({mode : 'closed'});
        priv.set (this, {shadowRoot: shadowRoot});
      }

      attributeChangedCallback (name, oldValue, value) {
        const shadowRoot = priv.get (this).shadowRoot;
        shadowRoot.innerHTML = '"' + name + '" change from ' +  oldValue + ' to ' + value;
      }
    }

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

    setTimeout(function() {
      const el = document.querySelector ('my-component');
      el.setAttribute ('num', 300);                        // Actualizar el atributo
    }, 1000);
  }
</script>

En este ejemplo escribimos en el Shadow DOM la información sobre el atributo num cuando este cambia de valor y es capturado por el método attributeChangedCallback().

Ejemplo

Todo lo que hemos estado viendo en las diferentes secciones de este artículo se pueden aplicar sin restricciones a los componentes web, simplemente utilizaremos this como elemento contenedor o host, el resto es exactamente igual. Hemos preparado un ejemplo algo más completo que se puede consultar en repl.it.

Conclusiones

Esperamos que a estas alturas esté bastante claro que conseguir el aislamiento del DOM y del CSS por medio del Shadow DOM es un requisito imprescindible para poder gestionar los componentes web sin miedo a conflictos y continuos problemas de acoplamiento y/o colisión de los elementos que cada componente contiene. Por ello el DOM en la sombra se ha hecho muy popular junto a los componentes web y debemos conocerlo a fondo.

Como hemos podido mostrar, esta es una característica que se puede aplicar a otros elementos HTML sin necesidad de crear un componente personalizado. Aunque esta forma de uso no suele ser muy habitual, nos ha servido para ver su comportamiento sin perdernos en otros aspectos de los Custom Elements.

A pesar de lo extenso de este artículo, hemos dejado para otra ocasión elementos relacionados, como el Light DOM, pero al menos hemos podido dedicar el suficiente espacio como para dar un repaso profundo a Shadow DOM y sus características. Esperamos que profundicéis en su funcionamiento.

Novedades

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

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

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

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

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.