Estandarización de las plantillas del cliente
Introducción
El concepto de plantillas no es nuevo en el desarrollo web. De hecho, los lenguajes o motores de plantillas del servidor, como Django (Python), ERB/Haml (Ruby) y Smarty (PHP), existen desde hace mucho tiempo. Sin embargo, en los últimos años, vimos una explosión de frameworks de MVC. Todos son un poco diferentes, pero la mayoría comparte un mecanismo común para renderizar su capa de presentación (también conocida como vista): las plantillas.
Enfrentémoslo. Las plantillas son fantásticas. Adelante, pregunta por ahí. Incluso su definición te hace sentir calidez y comodidad:
"…no tiene que volver a crearse cada vez…" No sé tú, pero a mí me encanta evitar el trabajo adicional. ¿Por qué la plataforma web carece de compatibilidad nativa con algo que claramente les importa a los desarrolladores?
La respuesta es la especificación de plantillas HTML de WhatWG. Define un elemento <template> nuevo que describe un enfoque estándar basado en el DOM para las plantillas del cliente. Las plantillas te permiten declarar fragmentos de lenguaje de marcado que se analizan como HTML, no se usan en la carga de página, pero se pueden instanciar
más adelante en el entorno de ejecución. Para citar a Rafael Weinstein:
Son un lugar para colocar un gran bloque de HTML con el que no quieres que el navegador interfiera en absoluto, por ningún motivo.
Rafael Weinstein (autor de la especificación)
Detección de atributos
Para detectar la función <template>, crea el elemento DOM y verifica que exista la propiedad .content:
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
Cómo declarar el contenido de la plantilla
El elemento HTML <template> representa una plantilla en tu lenguaje de marcado. Contiene
"contenido de plantilla"; en esencia, fragmentos inertes de DOM clonable.
Piensa en las plantillas como piezas de andamiaje que puedes usar (y reutilizar) durante
el ciclo de vida de tu app.
Para crear un contenido con plantilla, declara un lenguaje de marcado y, luego, insértalo en el elemento <template>:
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
Los pilares
Unir contenido en un <template> nos brinda algunas propiedades importantes.
Su contenido es inerte hasta que se activa. En esencia, tu lenguaje de marcado es un DOM oculto y no se renderiza.
El contenido de una plantilla no tendrá efectos secundarios. La secuencia de comandos no se ejecuta, las imágenes no se cargan, el audio no se reproduce… hasta que se usa la plantilla.
Se considera que el contenido no está en el documento. Si usas
document.getElementById()oquerySelector()en la página principal, no se devolverán los nodos secundarios de una plantilla.Las plantillas pueden colocarse en cualquier lugar dentro de
<head>,<body>o<frameset>, y pueden contener cualquier tipo de contenido que se permita en esos elementos. Ten en cuenta que "en cualquier lugar" significa que<template>se puede usar de forma segura en lugares que el analizador de HTML no permite… todos, excepto los elementos secundarios del modelo de contenido. También se puede colocar como un elemento secundario de<table>o<select>:
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
Activa una plantilla
Para usar una plantilla, debes activarla. De lo contrario, su contenido nunca se renderizará.
La forma más sencilla de hacerlo es crear una copia profunda de su .content con document.importNode(). La propiedad .content es un DocumentFragment de solo lectura que contiene el contenido de la plantilla.
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
Después de crear una plantilla, su contenido "se publica". En este ejemplo en particular, se clona el contenido, se realiza la solicitud de imagen y se renderiza el lenguaje de marcado final.
Demostraciones
Ejemplo: Secuencia de comandos inerte
En este ejemplo, se muestra la inercia del contenido de la plantilla. El <script> solo
se ejecuta cuando se presiona el botón, lo que estampa la plantilla.
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
Ejemplo: Crea un Shadow DOM a partir de una plantilla
La mayoría de las personas adjuntan el Shadow DOM a un host configurando una cadena de lenguaje de marcado en .innerHTML:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
El problema con este enfoque es que, cuanto más complejo se vuelve tu Shadow DOM,
más concatenación de cadenas realizas. No escala, las cosas se complican
rápido y los bebés empiezan a llorar. Este enfoque también es la forma en que nació XSS en primer lugar. <template> al rescate.
Una opción más razonable sería trabajar con el DOM directamente agregando contenido de plantilla a una raíz de sombra:
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
Gotchas
Estas son algunas de las dificultades que encontré cuando usé <template> en la práctica:
- Si usas modpagespeed, ten cuidado
con este error. Las plantillas
que definen
<style scoped>intercalado se pueden mover al encabezado con las reglas de reescritura de CSS de PageSpeed. - No hay forma de "renderizar previamente" una plantilla, lo que significa que no puedes cargar previamente recursos, procesar JS, descargar CSS inicial, etc. Esto se aplica tanto al servidor como al cliente. La plantilla solo se renderiza cuando se publica.
Ten cuidado con las plantillas anidadas. No se comportan como esperarías. Por ejemplo:
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>La activación de la plantilla externa no activará las plantillas internas. Es decir, las plantillas anidadas requieren que sus elementos secundarios también se activen de forma manual.
El camino hacia un estándar
No olvidemos de dónde venimos. El camino hacia las plantillas HTML basadas en estándares ha sido largo. A lo largo de los años, se nos han ocurrido algunos trucos bastante ingeniosos para crear plantillas reutilizables. A continuación, se muestran dos de las más comunes que encontré. Los incluyo en este artículo para compararlos.
Método 1: DOM fuera de pantalla
Un enfoque que se usa desde hace mucho tiempo es crear un DOM "fuera de la pantalla"
y ocultarlo de la vista con el atributo hidden o display:none.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
Si bien esta técnica funciona, tiene varias desventajas. A continuación, se presenta un resumen de esta técnica:
- Uso del DOM: El navegador conoce el DOM. Es buena en eso. Podemos clonarlo fácilmente.
- No se renderiza nada: agregar
hiddenimpide que se muestre el bloque. - No es inerte: Aunque nuestro contenido está oculto, se sigue realizando una solicitud de red para la imagen.
- Estilo y temas complejos: Una página de incorporación debe anteponer
#mytemplatea todas sus reglas de CSS para limitar los estilos a la plantilla. Esto es frágil y no hay garantías de que no se produzcan conflictos de nombres en el futuro. Por ejemplo, si la página de incorporación ya tiene un elemento con ese ID, no podremos hacer nada.
Método 2: Secuencia de comandos de sobrecarga
Otra técnica es sobrecargar <script> y manipular su contenido como una cadena. John Resig probablemente fue el primero en mostrar esto en 2008 con su utilidad de microplantillas.
Ahora hay muchos otros, incluidos algunos nuevos, como handlebars.js.
Por ejemplo:
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
A continuación, se presenta un resumen de esta técnica:
- No se renderiza nada: El navegador no renderiza este bloque porque
<script>esdisplay:nonede forma predeterminada. - Inert: El navegador no analiza el contenido de la secuencia de comandos como JS porque su tipo se establece en un valor distinto de "text/javascript".
- Problemas de seguridad: Fomenta el uso de
.innerHTML. El análisis de cadenas en el entorno de ejecución de los datos proporcionados por el usuario puede generar fácilmente vulnerabilidades de XSS.
Conclusión
¿Recuerdas cuando jQuery hizo que trabajar con el DOM fuera muy sencillo? El resultado fue que se agregaron querySelector()/querySelectorAll()
a la plataforma. Es una victoria obvia, ¿no? Una biblioteca popularizó la recuperación del DOM
con selectores CSS y los estándares la adoptaron más tarde. No siempre funciona así, pero me encanta cuando sucede.
Creo que <template> es un caso similar. Estandariza la forma en que creamos plantillas del lado del cliente, pero, lo que es más importante, elimina la necesidad de nuestros trucos de 2008.
En mi opinión, siempre es bueno hacer que todo el proceso de creación web sea más sensato, más fácil de mantener y más completo.
Recursos adicionales
- Especificación de WhatWG
- Introducción a los componentes web
- <web>components</web> (video): una presentación fantásticamente completa de tu servidor.