Nueva etiqueta de plantilla de HTML.

Estandariza 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, hemos visto una explosión de frameworks de MVC. Todas son ligeramente diferentes, pero la mayoría comparte una mecánica común para renderizar su capa de presentación (también conocida como vista): las plantillas.

Enfrentémoslo. Las plantillas son fantásticas. Adelante, pregúntale a alguien. Incluso su definición te hace sentir cálido y acogedor:

“…no se tiene que volver a crear cada vez…”. No sé tú, pero a mí me encanta evitar el trabajo adicional. Entonces, ¿por qué la plataforma web carece de compatibilidad nativa para algo que claramente les importa a los desarrolladores?

La especificación de Plantillas de HTML de WhatWG es la respuesta. Define un nuevo elemento <template> que describe un enfoque estándar basado en DOM para la creación de plantillas del cliente. Las plantillas te permiten declarar fragmentos de lenguaje de marcado que se analizan como HTML, que no se usan al cargar la página, pero se pueden crear instancias más adelante durante el tiempo de ejecución. En palabras de Rafael Weinstein:

Son un lugar para colocar un gran conjunto de HTML con el que no quieres que el navegador juegue en absoluto… por ningún motivo.

Rafael Weinstein (autor de las especificaciones)

Detección de atributos

Para detectar <template> por medio de funciones, 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.
}

Declara el contenido de la plantilla

El elemento <template> HTML representa una plantilla en tu marcado. Contiene "contenido de plantillas"; en esencia, fragmentos inertes de DOM clonables. Piensa en las plantillas como andamios que puedes usar (y reutilizar) durante todo el ciclo de vida de tu app.

Para crear un contenido basado en plantillas, declara un marcado y unelo en el elemento <template>:

<template id="mytemplate">
    <img src="" alt="great image">
    <div class="comment"></div>
</template>

Los pilares

Unir el contenido en un <template> nos brinda algunas propiedades importantes.

  1. Su contenido es inerte hasta que se activa. En esencia, tu marcado es un DOM oculto y no se renderiza.

  2. 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.

  3. Se considera que el contenido no está en el documento. Si usas document.getElementById() o querySelector() en la página principal, no se mostrarán los nodos secundarios de una plantilla.

  4. Las plantillas se pueden colocar en cualquier lugar dentro de <head>, <body> o <frameset>, y pueden incluir cualquier tipo de contenido que esté permitido 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… excepto en los elementos secundarios del modelo de contenido. También se puede colocar como elemento secundario de <table> o <select>:

<table>
  <tr>
    <template id="cells-to-repeat">
      <td>some content</td>
    </template>
  </tr>
</table>

Cómo activar 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 una DocumentFragment de solo lectura que contiene los detalles 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, el contenido se clona, se realiza la solicitud de la imagen y se renderiza el lenguaje de marcado final.

Demostraciones

Ejemplo: Guion inerte

Este ejemplo demuestra la inercia del contenido de la plantilla. <script> solo se ejecuta cuando se presiona el botón, lo que sella 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: Cómo crear Shadow DOM a partir de una plantilla

La mayoría de las personas adjuntan Shadow DOM a un host configurando una cadena de marcado en .innerHTML:

<div id="host"></div>
<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.innerHTML = '<span>Host node</span>';
</script>

El problema de este enfoque es que cuanto más complejo sea el Shadow DOM, más concatenación de cadenas realizarás. No se escala, todo se desordena rápido y los bebés comienzan a llorar. Con este enfoque, también nació XSS en primer lugar. <template> al rescate.

Algo más sensato sería trabajar con DOM directamente agregando contenido de plantillas a una shadow root:

<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>

Problemas

A continuación, se muestran algunos inconvenientes que encontré cuando usé <template> de forma segura:

  • Si usas modpagespeed, ten cuidado con este error. Las plantillas que definen <style scoped> intercalados, muchas se mueven a la cabeza con las reglas de reescritura de CSS de PageSpeed.
  • No hay forma de “renderizar previamente” una plantilla, lo que significa que no puedes precargar recursos, procesar JS, descargar CSS inicial, etcétera. Esto se aplica tanto al servidor como al cliente. El único momento en que se renderiza una plantilla es cuando se publica.
  • Ten cuidado con las plantillas anidadas. No se comportan como esperas. Por ejemplo:

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    Si activas la plantilla externa, no se activarán 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 fue largo. A lo largo de los años, hemos creado trucos para crear plantillas reutilizables. A continuación, se muestran dos ejemplos comunes que encontré. Las incluyo en este artículo a modo de comparación.

Método 1: DOM fuera de la pantalla

Un enfoque que las personas usan desde hace mucho tiempo es crear un DOM "fuera de la pantalla" y ocultarlo 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. Resumen de esta técnica:

  • Con DOM: el navegador conoce DOM. Es bueno. Podemos clonarlo fácilmente.
  • No se renderiza nada: Si agregas hidden, se evita que se muestre el bloque.
  • No es inerte: Aunque nuestro contenido está oculto, se sigue realizando una solicitud de red para la imagen.
  • Estilos y temas minuciosos: Una página de incorporación debe agregar el prefijo #mytemplate a todas sus reglas de CSS para limitar los estilos a la plantilla. Esto es inestable y no hay garantías de que no encontremos conflictos de nombres en el futuro. Por ejemplo, se nos mantiene si la página de incorporación ya tiene un elemento con ese ID.

Método 2: Secuencia de comandos de sobrecarga

Otra técnica es sobrecargar <script> y manipular su contenido como una cadena. John Resig fue probablemente 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>

Resumen de esta técnica:

  • No se renderiza nada: El navegador no renderiza este bloque porque <script> es display:none de forma predeterminada.
  • Inerte: El navegador no analiza el contenido de la secuencia de comandos como JS porque su tipo está configurado en algo distinto de "text/javascript".
  • Problemas de seguridad: Se recomienda usar .innerHTML. El análisis de cadenas en el tiempo 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 agregó querySelector()/querySelectorAll() a la plataforma. Una victoria obvia, ¿no? Una biblioteca popularizó la recuperación de DOM con selectores y estándares de CSS, y más tarde la adoptó. No siempre funciona de esa manera, pero me encanta cuando lo hace.

Creo que <template> es un caso similar. Estandariza la forma en que hacemos la creación de plantillas del cliente, pero lo más importante es que elimina la necesidad de nuestros hacks de 2008. En mi opinión, siempre es bueno hacer que todo el proceso de creación de páginas web sea más sencillo, más fácil de mantener y más completo.

Recursos adicionales