Introducción
Los componentes web son un conjunto de estándares de vanguardia que permiten lo siguiente:
- Hacer posible la compilación de widgets
- …que se pueden reutilizar de forma confiable
- …y que no dañará las páginas si la próxima versión del componente cambia los detalles de implementación internos.
¿Esto significa que tienes que decidir cuándo usar HTML/JavaScript y cuándo usar componentes web? ¡No! HTML y JavaScript pueden crear elementos visuales interactivos. Los widgets son elementos visuales interactivos. Tiene sentido aprovechar tus habilidades de HTML y JavaScript cuando desarrollas un widget. Los estándares de Web Components están diseñados para ayudarte a lograrlo.
Sin embargo, hay un problema fundamental que dificulta el uso de los widgets creados con HTML y JavaScript: el árbol del DOM dentro de un widget no está encapsulado del resto de la página. Esta falta de encapsulamiento significa que la hoja de estilo de tu documento podría aplicarse accidentalmente a partes dentro del widget, que tu JavaScript podría modificar accidentalmente partes dentro del widget, que tus IDs podrían superponerse con los IDs dentro del widget, etcétera.
Los componentes web constan de tres partes:
El Shadow DOM aborda el problema de encapsulamiento del árbol del DOM. Las cuatro partes de los componentes web están diseñadas para funcionar juntas, pero también puedes elegir qué partes de los componentes web usar. En este instructivo, se muestra cómo usar el DOM de shadow.
Hello, Shadow World
Con el Shadow DOM, los elementos pueden obtener un nuevo tipo de nodo asociado a ellos. Este nuevo tipo de nodo se denomina raíz de sombra. Un elemento que tiene una raíz de sombra asociada se denomina host de sombra. No se renderiza el contenido de un host de shadow, sino el de la raíz de shadow.
Por ejemplo, si tuvieras un lenguaje de marcado como este:
<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>
entonces, en lugar de
<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
Array.prototype.forEach.call(
document.querySelectorAll(selector),
function (node) { node.parentNode.removeChild(node); });
}
if (!HTMLElement.prototype.createShadowRoot) {
remove('#ex1a');
document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>
tu página se ve de la siguiente manera:
<button id="ex1b">Hello, world!</button>
<script>
(function () {
if (!HTMLElement.prototype.createShadowRoot) {
remove('#ex1b');
document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
return;
}
var host = document.querySelector('#ex1b');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
})();
</script>
No solo eso, si JavaScript en la página pregunta cuál es el textContent del botón, no obtendrá “こんにちは、影の世界!”, sino “Hello, world!”, porque el subárbol del DOM debajo de la raíz de sombra está encapsulado.
Separar el contenido de la presentación
Ahora veremos cómo usar el Shadow DOM para separar el contenido de la presentación. Supongamos que tenemos esta etiqueta de nombre:
<style>
.ex2a.outer {
border: 2px solid brown;
border-radius: 1em;
background: red;
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
}
.ex2a .boilerplate {
color: white;
font-family: sans-serif;
padding: 0.5em;
}
.ex2a .name {
color: black;
background: white;
font-family: "Marker Felt", cursive;
font-size: 45pt;
padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
Este es el lenguaje de marcado. Esto es lo que escribirías hoy. No usa Shadow DOM:
<style>
.outer {
border: 2px solid brown;
border-radius: 1em;
background: red;
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
}
.boilerplate {
color: white;
font-family: sans-serif;
padding: 0.5em;
}
.name {
color: black;
background: white;
font-family: "Marker Felt", cursive;
font-size: 45pt;
padding-top: 0.2em;
}
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
Debido a que el árbol del DOM carece de encapsulamiento, toda la estructura de la etiqueta de nombre se expone al documento. Si otros elementos de la página usan accidentalmente los mismos nombres de clase para el diseño o las secuencias de comandos, tendremos problemas.
Podemos evitar pasar un mal momento.
Paso 1: Oculta los detalles de la presentación
Semánticamente, es probable que solo nos importe lo siguiente:
- Es una etiqueta personal.
- El nombre es “Bob”.
Primero, escribimos un lenguaje de marcado que se acerque más a la semántica real que queremos:
<div id="nameTag">Bob</div>
Luego, colocamos todos los estilos y divs que se usan para la presentación en un elemento <template>:
<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
border: 2px solid brown;
… same as above …
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div></span>
</template>
En este punto, “Bob” es lo único que se renderiza. Como
movimos los elementos del DOM de presentación dentro
de un elemento <template>, no se renderizan, pero
se puede acceder a ellos desde JavaScript. Haremos eso ahora para
completar la raíz de sombra:
<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);
Ahora que configuramos una raíz de shadow, la etiqueta de nombre se vuelve a renderizar. Si haces clic con el botón derecho en la etiqueta personal y examinas el elemento, verás que es un lenguaje de marcado semántico y agradable:
<div id="nameTag">Bob</div>
Esto demuestra que, con el Shadow DOM, ocultamos los detalles de presentación de la etiqueta de nombre del documento. Los detalles de la presentación se encapsulan en el Shadow DOM.
Paso 2: Separa el contenido de la presentación
Nuestra etiqueta personal ahora oculta los detalles de presentación de la página, pero no separa la presentación del contenido, ya que, aunque el contenido (el nombre “Bob”) está en la página, el nombre que se renderiza es el que copiamos en la raíz de shadow. Si quisiéramos cambiar el nombre en la etiqueta, tendríamos que hacerlo en dos lugares, y podrían desincronizarse.
Los elementos HTML son composicionales, es decir, puedes colocar un botón dentro de una tabla, por ejemplo. Aquí necesitamos la composición: la etiqueta personal debe ser una composición del fondo rojo, el texto “Hi!” y el contenido que se encuentra en la etiqueta personal.
Tú, el autor del componente, defines cómo funciona la composición con tu widget usando un elemento nuevo llamado <content>. Esto crea un punto de inserción en la presentación del widget, y el punto de inserción selecciona contenido del host de sombra para presentarlo en ese punto.
Si cambiamos el lenguaje de marcado en el Shadow DOM por el siguiente:
<span class="unchanged"><template id="nameTagTemplate">
<style>
…
</style></span>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
<content></content>
</div>
</div>
<span class="unchanged"></template></span>
Cuando se renderiza la etiqueta personal, el contenido del host de sombra se proyecta en el lugar donde aparece el elemento <content>.
Ahora, la estructura del documento es más simple porque el nombre solo está en un lugar: el documento. Si tu página alguna vez necesita actualizar el nombre del usuario, solo debes escribir lo siguiente:
document.querySelector('#nameTag').textContent = 'Shellie';
y eso es todo. El navegador actualiza automáticamente la renderización de la etiqueta personal, ya que proyectamos el contenido de la etiqueta personal en su lugar con <content>.
<div id="ex2b">
Ahora logramos separar el contenido y la presentación. El contenido está en el documento; la presentación está en el Shadow DOM. El navegador las mantiene sincronizadas automáticamente cuando llega el momento de renderizar algo.
Paso 3: Obtén ganancias
Si separamos el contenido y la presentación, podemos simplificar el código que manipula el contenido. En el ejemplo de la etiqueta personal, ese código solo necesita tratar con una estructura simple que contenga un <div> en lugar de varios.
Ahora, si cambiamos nuestra presentación, no necesitamos cambiar nada del código.
Por ejemplo, supongamos que queremos localizar nuestra etiqueta personal. Sigue siendo una etiqueta de nombre, por lo que el contenido semántico del documento no cambia:
<div id="nameTag">Bob</div>
El código de configuración de la raíz de sombra sigue siendo el mismo. Solo cambia lo que se coloca en la raíz de sombra:
<template id="nameTagTemplate">
<style>
.outer {
border: 2px solid pink;
border-radius: 1em;
background: url(sakura.jpg);
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
font-family: sans-serif;
font-weight: bold;
}
.name {
font-size: 45pt;
font-weight: normal;
margin-top: 0.8em;
padding-top: 0.2em;
}
</style>
<div class="outer">
<div class="name">
<content></content>
</div>
と申します。
</div>
</template>
Esto es una gran mejora con respecto a la situación actual en la Web, ya que el código de actualización de nombres puede depender de la estructura del componente, que es simple y coherente. El código de actualización de tu nombre no necesita conocer la estructura que se usa para la renderización. Si consideramos lo que se renderiza, el nombre aparece en segundo lugar en inglés (después de “Hi! My name is”), pero primero en japonés (antes de “と申します”). Esa distinción no tiene sentido semántico desde el punto de vista de la actualización del nombre que se muestra, por lo que el código de actualización del nombre no tiene que conocer ese detalle.
Crédito adicional: Proyección avanzada
En el ejemplo anterior, el elemento <content>
selecciona todo el contenido del host de sombra. Con el atributo select, puedes controlar lo que proyecta un elemento de contenido. También puedes usar varios elementos de contenido.
Por ejemplo, si tienes un documento que contiene lo siguiente:
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
y una raíz de sombra que usa selectores CSS para seleccionar contenido específico:
<div style="background: purple; padding: 1em;">
<div style="color: red;">
<content **select=".first"**></content>
</div>
<div style="color: yellow;">
<content **select="div"**></content>
</div>
<div style="color: blue;">
<content **select=".email">**</content>
</div>
</div>
Los elementos <content select="div"> y <content
select=".email"> coinciden con el elemento <div class="email">. ¿Cuántas veces aparece la dirección de correo electrónico de Bob y en qué colores?
La respuesta es que la dirección de correo electrónico de Bob aparece una vez y es de color amarillo.
El motivo es que, como saben las personas que hackean el Shadow DOM,
construir el árbol de lo que realmente se renderiza en la pantalla es como una
gran fiesta. El elemento de contenido es la invitación que permite
que el contenido del documento ingrese a la fiesta de renderización
del Shadow DOM en el backstage. Estas invitaciones se entregan en orden; quién recibe una
invitación depende de a quién se dirige (es decir,
el atributo select). Una vez que se invita al contenido, este siempre acepta la invitación (¿quién no lo haría?) y se va. Si se vuelve a enviar una invitación posterior a esa dirección,
no hay nadie en casa y no llega a tu fiesta.
En el ejemplo anterior, <div class="email"> coincide con el selector div y el selector .email, pero como el elemento de contenido con el selector div aparece antes en el documento, <div class="email"> va a la fiesta amarilla y nadie está disponible para ir a la fiesta azul. (Esa podría ser la razón por la que es tan azul, aunque el mal de muchos es consuelo de tontos, así que nunca se sabe).
Si no se invita a ninguna parte, no se renderiza en absoluto. Eso es lo que sucedió con el texto “Hello, world” en el primer ejemplo. Esto es útil cuando deseas lograr una renderización radicalmente diferente: escribe el modelo semántico en el documento, que es lo que está disponible para las secuencias de comandos en la página, pero ocúltalo para fines de renderización y conéctalo a un modelo de renderización realmente diferente en Shadow DOM con JavaScript.
Por ejemplo, HTML tiene un buen selector de fecha. Si escribes <input
type="date">, obtendrás un calendario emergente ordenado. Pero ¿qué pasa si quieres permitir que el usuario elija un rango de fechas para sus vacaciones en una isla de postres (ya sabes, con hamacas hechas de Red Vines)? Configuras
tu documento de la siguiente manera:
<div class="dateRangePicker">
<label for="start">Start:</label>
<input type="date" name="startDate" id="start">
<br>
<label for="end">End:</label>
<input type="date" name="endDate" id="end">
</div>
pero crea un Shadow DOM que use una tabla para crear un calendario elegante que destaque el rango de fechas, etcétera. Cuando el usuario hace clic en los días del calendario, el componente actualiza el estado en las entradas startDate y endDate; cuando el usuario envía el formulario, se envían los valores de esos elementos de entrada.
¿Por qué incluí etiquetas en el documento si no se van a renderizar? Esto se debe a que, si un usuario ve el formulario con un navegador que no admite el Shadow DOM, el formulario se puede usar, pero no es tan atractivo. El usuario ve algo como lo siguiente:
<div class="dateRangePicker">
<label for="start">Start:</label>
<input type="date" name="startDate" id="start">
<br>
<label for="end">End:</label>
<input type="date" name="endDate" id="end">
</div>
Aprobaste Shadow DOM 101
Esos son los conceptos básicos de Shadow DOM. ¡Aprobaste el curso introductorio! Puedes hacer más con el DOM de sombra. Por ejemplo, puedes usar varias sombras en un host de sombra o sombras anidadas para la encapsulación, o bien diseñar tu página con vistas basadas en modelos (MDV) y el DOM de sombra. Y los componentes web son más que solo el Shadow DOM.
Los explicamos en publicaciones posteriores.