¿Cómo crear un componente web? templates (plantillas) y shadow DOM

¿Como logramos encapsulación de nuestros elementos HTML, estilos y JS para crear una pieza de una aplicación web que no provoque conflictos con el resto del sistema?, la respuesta es gracias al tag <template>, al Shadow DOM y a los custom elements. Antes de comenzar, si aún no estas familiarizado con los custom elements y los componentes web nativos en general, te recomiendo revisar primero estas publicaciones:

<template>

Primero vamos a ver que es template o plantilla para posteriormente reutilizar esa plantilla en un custom element con shadow DOM. Un template o plantilla en el mundo web es una forma de crear elementos visuales de manera sencilla y eficiente, de tal forma que se pueden definir partes de una pagina web o aplicación a las cuales les podemos insertar valores. Este template puede ser reutilizado en diferentes vistas de una aplicación. Tal vez estés familiarizado con jade, pug, mustache, o con algunos frameworks y librerias como angular y vue, los cuales utilizan templates. También si has utilizado wordpress y php, los archivos de los temas serian una especie de templates.

Ahora, los templates que vamos a aprender son específicos de los navegadores web, para entrar un poco más en contexto veamos un ejemplo sencillo de como podemos crear varios elementos para mostrar un saludo, utilizaremos el API del DOM:

  • document.createDocumentFragment()
  • document.createElement(‘nombreTag’, [opciones])
  • nodo.appendChild(nodo)

Queremos mostrar en nuestra aplicación un saludo al usuario y lo hacemos de la siguiente forma:

Ahora veamos como hacer lo mismo, pero utilizando la etiqueta <template>:

Como podemos ver usando la etiqueta <template> se usa mucho menos código, si el contenido de lo que queremos mostrar crece, solo debemos agregar las etiquetas y demás contenido de manera declarativa dentro de la etiqueta <template> pero si usamos el primer método nuestro código JS crecerá mucho más, esto es más difícil de entender y mantener con el tiempo.

Como seguro habrás notado, no utilizamos la propiedad innerHTML para agregar nuestro contenido porque esta propiedad es lenta, debe parsear nuestro string html, luego crear los elementos e insertarlos en el árbol de nodos. Lo más costoso en una aplicación web es el pintado de los elementos html, esto es así porque debe calcular las posiciones y tamaños en relación de los demás elementos que lo rodean, luego hacer calculo de los estilos finales de las reglas css que afecten a estos elementos y por último pintarlos pixel por pixel. Dentro de estos tres paso existen muchos cálculos matemáticos y demás carga de trabajo por lo que es muy importante disminuir en la medida de lo posible este tipo de tareas.

La ventaja de utilizar document fragments es que su contenido no es aún agregado al árbol de nodos, sino que se encuentra en memoria y se inserta una sola ves al final, esto quiere decir que los tres pasos antes mencionadas se ejecutan una sola vez por todos los elementos que creamos.

// Agregar document fragment a la página 
host.appendChild(df);

Existen 4 puntos importantes al utilizar la etiqueta <template> para crear plantillas:

  • La etiqueta <template> no se pinta en nuestra aplicación, por default tiene un display: none;.
  • El contenido de la etiqueta <template> es un document fragment, el cual tiene la característica de no estar insertado en el árbol de nodos. Esto es bueno para el rendimiento de la aplicación debido al alto costo de pintar elementos.
  • El código JS necesario es muy simple y corto
  • Cuando clonamos el contenido de nuestro template usando cloneNode(), debemos pasar el valor true como parámetro, de otra manera no clonaríamos los elementos hijos del document fragment de nuestro template.

Shadow DOM

Posiblemente te preguntaras, ¿Y ahora esto que tiene que ver con los componentes web?, en un momento lo vemos, pero para eso debemos de saber que es el Shadow DOM. El shadow DOM es un DOM o árbol de nodos en las sombras, escondidos de los demás elementos de una aplicación. Para entender esto vamos a crear nuestro primer componente web utilizando el estándar de custom elements, la etiqueta <template> y el shadow DOM.

Nuestro componente se llama mi-saludo, con el contenido del ejemplo anterior:

Si aún no sabes que son los custom elements, sigue este link. En el ejemplo anterior creamos un custom element llamado mi-saludo, dentro del constructor() creamos una instancia del template.

// Obtengo la única etiqueta 'template'
const tpl = document.querySelector('template');
// Clono su contenido y se crea una instancia del document fragment
const tplInst = tpl.content.cloneNode(true);

Luego creamos un shadow DOM y lo adjuntamos al custom element mi-saludo:

// Se crea un shado dom para las instancias de mi-saludo
this.attachShadow({ mode: 'open'});

Y finalmente agregamos el contenido clonado del template dentro del shadow DOM:

// Y se agrega el template dentro del shadow DOM usando el elemento raíz 'shadowRoot'
this.shadowRoot.appendChild(tplInst);

En este último paso usamos la propiedad shadowRoot, esta propiedad hace referencia a un nodo raíz especial de donde se empieza a colgar todo lo que definimos dentro de nuestra etiqueta <template>, y es a partir de este nodo que agregamos contenido.

Lo que se agrega al shadow DOM esta oculto para el exterior, ningún document.querySelector() externo puede acceder a los elementos del shadow DOM y los estilos definidos en el exterior no pueden acceder a estos elementos ocultos. Esto quiere decir que dentro del shadow DOM podemos utilizar atributos id y class sin temor a colosionar con los ids y clases del exterior.

Cuando creamos un shadow DOM, se hace con la propiedad mode: open, de esta manera se puede acceder al contenido del shadow DOM usando JS y el atributo shadowRoot. Sin el atributo shadowRoot es imposible acceder a los elementos y eso nos proporciona encapsulación de nuestro componente.

Para crear una nueva instancia de nuestro nuevo componente, solo utilizamos el nombre que registramos como etiqueta:

// Se registra el custom element para poder ser utilizado declarativamente en el HTML o imperativamente mediante JS
customElements.define('mi-saludo', MiSaludo);

Y en el html utilizamos código declarativo:

<mi-saludo></mi-saludo>

¿Que pasa si a nuestro template le agremos una etiqueta style? Debido a que las instancias de nuestro template son agregadas al shadow DOM, entonces esos estilos solo afectan dentro del shadow DOM.

Para establecer estilos dentro del shadow DOM al nuevo tag creado, se utiliza la pseudoclase :host, que hace referencia a la etiqueta huésped del shadow DOM, es decir, mi-saludo.

También agregamos en el exterior estilos para las etiquetas <h1>, estos estilos no pudieron afectar al contenido interno del shadow DOM, el único h1#mi-id con borde rojo es el que se encuentra fuera de nuestro componente. El h1 externo como el interno tienen el mismo id, a causa del encapsulamiento con shadow DOM, no colisionan los estilos con el mismo selector de id.

Ahora en nuestro código html, agreguemos más etiquetas <mi-saludo:

Como podemos ver la combinación de estas tres tecnologías; custom elements, template y shadow DOM nos permite crear nuevas etiquetas personalizadas con su propio contenido interno sin afectar el exterior y también el exterior no puede afectar directamente la implementación interna de nuestro componente. En el último ejemplo podemos ver como creamos cuatro instancias de nuestro componente simplemente utilizando etiquetas, es decir, reutilizando nuestro componente encapsulado.

En la próxima publicación veremos como implementar más funcionalidad Javascript encapsulada del componente web y también más características muy útiles del shadow DOM como composición, eventos y más estilos. Finalmente veremos HTML imports y modulos de JS para completar un componente web reutilizable que pueda ser importado en cualquier otra aplicación web.

Artículo relacionado:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

This site uses Akismet to reduce spam. Learn how your comment data is processed.