Nueva etiqueta de plantilla de HTML.

Estandariza las plantillas del cliente

Introducción

El concepto de plantilla 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 surgir una explosión de frameworks de MVC. Todos son ligeramente diferentes; sin embargo, la mayoría comparten una mecánica común para renderizar su capa de presentación (también conocida como da view): las plantillas.

Enfrentémoslo. Las plantillas son fantásticas. Adelante, pregunta a tu alrededor. Incluso su definición te hace sentir cálido y cómodo:

"...no tiene que recrearse cada vez..." No sé de ti, pero me encanta evitar el trabajo extra. ¿Por qué la plataforma web carece de compatibilidad nativa para algo que claramente les importa a los desarrolladores?

La respuesta es la especificación de las plantillas HTML de WhatsApp. Define un nuevo elemento <template> que describe un enfoque estándar basado en DOM para las plantillas del cliente. Las plantillas te permiten declarar fragmentos de lenguaje de marcado que se analizan como HTML y no se usan durante la carga de la página, pero de los que se puede crear una instancia más adelante durante el tiempo de ejecución. Cita a Rafael Weinstein:

Son un lugar para poner una gran cantidad de código HTML con el que no querrás que el navegador interfiera en absoluto... por ningún motivo.

Rafael Weinstein (autor de la especificación)

Detección de atributos

Para detectar <template>, crea el elemento DOM y verifica que la propiedad .content exista:

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 el lenguaje de marcado. Contiene "contenido de plantilla"; esencialmente, fragmentos inertes de DOM clonables. Piensa en las plantillas como un andamiaje que puedes usar (y reutilizar) durante todo el ciclo de vida de tu app.

Para crear contenido basado en una plantilla, declara algo de lenguaje de marcado y únelo al elemento <template>:

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

Los pilares

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

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

  2. El contenido de una plantilla no tendrá efectos secundarios. No se ejecuta la secuencia de comandos, las imágenes no se cargan, el audio no se reproduce... hasta que se use la plantilla.

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

  4. Las plantillas pueden colocarse en cualquier lugar de <head>, <body> o <frameset>, y pueden incluir cualquier tipo de contenido 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... todos los elementos secundarios, excepto el modelo de contenido. También se puede ubicar como 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 mediante document.importNode(). La propiedad .content es un DocumentFragment de solo lectura que contiene los elementos esenciales 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 sellar una plantilla, su contenido se publica. En este ejemplo concreto, se clona el contenido, se realiza la solicitud de la imagen y se renderiza el lenguaje de marcado final.

Demostraciones

Ejemplo: Secuencia de comandos inerte

En este ejemplo, se demuestra la inercia del contenido de la plantilla. El <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 un Shadow DOM a partir de una plantilla

La mayoría de los usuarios adjuntan Shadow DOM a un host estableciendo una cadena de lenguaje de marcado como .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 sea tu Shadow DOM, más concatenación de cadenas realizarás. No escala, las cosas se desordenan rápidamente y los bebés empiezan a llorar. Este enfoque también es la forma en que nació XSS en primer lugar. <template>, al rescate.

Algo más adecuado sería trabajar directamente con DOM agregando contenido de la plantilla 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>

Trampas

A continuación, se muestran algunos de los desafíos que encontré cuando uso <template> en el entorno:

  • Si usas modpagespeed, ten cuidado con este error. Las plantillas que definen <style scoped> intercalados, muchas de ellas 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. Esto es así tanto para el servidor como para el cliente. Una plantilla solo se renderiza 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 se activa 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 a 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, hemos ideado algunos trucos ingeniosos para crear plantillas reutilizables. A continuación, aparecen dos comunes con las que me he encontrado. Los incluyo en este artículo para compararlos.

Método 1: DOM fuera de pantalla

Un enfoque que las personas han usado por mucho tiempo es crear un DOM "fuera de la pantalla" y ocultarlo a través del atributo hidden o display:none.

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

Si bien esta técnica funciona, existen algunas desventajas. Este es el resumen de esta técnica:

  • Con DOM: el navegador reconoce DOM. Lo hace bien. Podemos clonarla fácilmente.
  • No se renderiza nada: Si agregas hidden, no se mostrará el bloque.
  • No inerte: Aunque el contenido esté oculto, se realiza una solicitud de red para la imagen.
  • Estilos y temas difíciles: Una página de incorporación debe anteponer #mytemplate a todas sus reglas de CSS para definir el alcance de los estilos hasta la plantilla. Esta es frágil y no hay garantías de que no encontraremos futuras colisiones de nombres. Por ejemplo, se genera un impacto si la página de incorporación ya tiene un elemento con ese ID.

Método 2: Sobrecarga la secuencia de comandos

Otra técnica es sobrecargar <script> y manipular su contenido como una cadena. Probablemente, John Resig fue el primero en mostrar esto en 2008 con su utilidad de microplantillas. Ahora hay muchos otros, incluidos algunos elementos secundarios nuevos en el bloque, como handlebars.js.

Por ejemplo:

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

Este es el 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 que no es "text/javascript".
  • Problemas de seguridad: Fomenta el uso de .innerHTML. El análisis de cadenas en tiempo de ejecución de los datos proporcionados por el usuario puede conducir fácilmente a vulnerabilidades XSS.

Conclusión

¿Recuerdas cuando jQuery hizo que trabajar con DOM no fuera sencillo? Como resultado, se agregó querySelector()/querySelectorAll() a la plataforma. Es una victoria obvia, ¿verdad? Una biblioteca popularizó la recuperación de DOM con selectores CSS y estándares más adelante. No siempre funciona así, pero me encanta cuando lo hace.

Creo que <template> es un caso similar. Estandariza la forma en que realizamos las plantillas del cliente, pero lo más importante es que elimina la necesidad de nuestros hackeos de 2008. Siempre es buena idea hacer que todo el proceso de creación web sea más ordenado, fácil de mantener y más completo.

Recursos adicionales