Seleccionar página
Hoy vamos a profundizar en dos formas de crear contenido en un componente, aunque ambas se llaman template son muy diferentes entre sí. Una de ellas, la etiqueta template, se considera como uno de los pilares de los Web Components; la otra, las template string de Javascript, son una buena alternativa para generar el Shadow DOM con interpolación de datos.

Si alguno conoce las soluciones que aportan los frameworks modernos en Javascript para la creación del contenido de los componentes por medio de plantillas, es muy posible que se quede bastante defraudado con las posibilidades que ofrece el estándar. Probablemente tenga bastante razón, el resto de las funcionalidades que se consideran parte de los Web Component estándar son equivalentes e incluso, en algunos casos, más completas que las que ofrecen los frameworks, pero en el caso de las plantillas para generar el HTML las opciones que tenemos en el estándar son bastante limitadas.

No obstante, las posibilidades que nos ofrecen tanto la etiqueta template como los Template String, bien gestionadas, dan bastante buen resultado en nuestros componentes, así que vamos a analizarlas en profundidad.

Etiqueta template

La etiqueta HTML template es un mecanismo para contener un fragmento de HTML que se carga en la página, pero no se renderiza. Cuando sea necesario este contenido puede ser clonado, modificado e insertado en el documento. Es decir, se está creando es una plantilla como un fragmento de contenido que está siendo interpretada para un uso posterior. Los navegadores procesan y crean cada uno de los elementos presentes en el template, pero el elemento no se visualiza en la pantalla.

Como en el resto de las tecnologías asociadas a los Web Components la etiqueta template se puede utilizar con y sin custom elements, por lo que se puede aplicar sin problemas sobre cualquier elemento del DOM.

<template id="tmplt">
  <h2>Template content</h2>
</template>
<button id="clone">Clone and insert template</button>
<div id="result"></div>
<script>
  document.querySelector ('#clone').addEventListener ('click', () => {
    const template = document.querySelector ('#tmplt');
    const clone    = template.content.cloneNode(true);
    document.querySelector ('#result').appendChild (clone);
  })
</script>

Como se puede ver en este ejemplo, el contenido de la etiqueta template no es visualizado en la página. Cuando se pulsa el botón, entonces podemos clonar su contenido con .content.cloneNode(true) e insértalo en otro elemento.

La gran ventaja de template es que el contenido está completamente interpretado por el navegador y su inserción muy eficiente, mucho más rápida que si utilizamos .innerHTML e insertamos el contenido en forma de cadena de texto. La etiqueta template puede contener fragmentos de HTML parciales, como por ejemplo una fila con el elemento tr, lo cual puede ser muy útil para construir una tabla.

<table>
  <thead><tr><th>Flag</th><th>Country</th></tr></thead>
  <tbody></tbody>
</table>
<template id="row">
  <tr><td></td><td></td></tr>
</template>
<script>
  const path  = 'https://raw.githubusercontent.com/hjnilsson/country-flags/master';
  const row   = document.querySelector ('#row');
  const tbody = document.querySelector ('table > tbody');
  fetch (`${path}/countries.json`)
    .then (response => {
      if (response.status === 200) {
        return response.json ();
      }
    })
    .then (data => {
      for (let country in data) {
        const clone     = row.content.cloneNode (true);
        const tds       = clone.querySelectorAll ('td');
        const img       = document.createElement ('img');
        img.src         = `${path}/png100px/${ country.toLowerCase () }.png`;
        img.style.width = '40px';
        tds[ 0 ].appendChild (img);
        tds[ 1 ].textContent = data[ country ];
        tbody.appendChild (clone);
      }
    });
</script>

Como se puede observar en el ejemplo, el contenido de la plantilla se puede ajustar y completar antes de insertar su contenido en otro elemento del DOM. De ahí que sea importante realizar una clonación del contenido antes de manipularlo e insertarlo.

Es posible utilizar más de una etiqueta template y combinar sus contenidos en Javascript. En el siguiente ejemplo vamos a reescribir el código anterior para que tanto la tabla como la fila sean plantillas que se utilizan desde un componente web:

<template id="table">
  <table>
    <thead><tr><th>Flag</th><th>Country</th></tr></thead>
    <tbody></tbody>
  </table>
</template>
<template id="row">
  <tr><td></td><td></td></tr>
</template>
<script>
  class WCCountries extends HTMLElement {
    constructor () {
      super ();
      this.attachShadow ({mode : 'open'});
      const path  = 'https://raw.githubusercontent.com/hjnilsson/country-flags/master';
      const table       = document.querySelector('#table').content.cloneNode(true);
      const tbody       = table.querySelector ('tbody');
      const rowTemplate = document.querySelector ('#row');
      this.shadowRoot.appendChild (table);
      fetch (`${path}/countries.json`)
        .then (response => {
          if (response.status === 200) {
            return response.json ();
          }
        })
        .then (data => {
          for (let country in data) {
            const clone     = row.content.cloneNode (true);
            const tds       = clone.querySelectorAll ('td');
            const img       = document.createElement ('img');
            img.src         = `${path}/png100px/${ country.toLowerCase () }.png`;
            img.style.width = '40px';
            tds[ 0 ].appendChild (img);
            tds[ 1 ].textContent = data[ country ];
            tbody.appendChild (clone);
          }
        });
    }
  }
  customElements.define('wc-countries', WCCountries);
</script>
<wc-countries></wc-countries>

En la práctica no se utiliza mucho la etiqueta template en los componentes web, pero no es culpa suya, desde que ha quedado deprecated el HTML Import, no es sencillo encontrar una forma de cargar fragmentos de HTML que contengan las plantillas. Insertar la etiqueta template de nuestro componente dentro del DOM principal de la página no es muy realista, se puede aceptar para un sencillo ejemplo, pero en proyectos reales vamos a querer que nuestros componentes estén definidos en ficheros independientes.

Es cierto que también se puede crear un elemento template por medio de document.createElement(). Esto puede parecer útil cuando nuestro componente es importado como un fichero .js. Si hacemos uso del método createElement(), nos basta con crear los diferentes elementos directamente y no necesitamos un elemento template. Podemos encontrar bastantes ejemplos de document.createElement("template"), pero son consecuencia de la búsqueda de una solución a carga dinámica de las plantillas desde Javascript. En un próximo artículo analizaremos este problema y algunas de las posibles soluciones con mayor detalle.

Conocer las capacidades de la etiqueta template es importante, pero su funcionalidad es bastante limitada, por lo que no soluciona todos los retos que nos encontramos a la hora de construir componentes. Vamos a ver alguna otra opción:

Template String

Las Template String son parte de EcmaScript 6 y ofrecen un sistema de plantillas para interpolación de datos, integrado en el propio lenguaje. Normalmente no se considera Template String como una parte fundamental de los Web Components ya que es una funcionalidad de Javascript y no del HTML o el DOM. No obstante, son una solución bastante interesante y efectiva de interpolar etiquetas y datos en una cadena de texto con la que podemos construir el DOM de nuestros componentes.

Las Template String parecen cadenas de texto normales, pero que utilizan comillas (`) en lugar de las comillas habituales (' ó "). El intérprete entiende que es una plantilla y permiten el uso de expresiones incrustadas por medio de ${ ... } y la utilización de cadenas de texto de más de una línea, todo ello de forma muy eficiente.

Vamos a dar un repaso a sus principales características y a cómo podemos aprovecharlas en nuestros componentes web.

Contenido dinámico

Las plantillas permite combinar texto estático con valores por medio de ${ expresión }. El contenido puede ser cualquier expresión valida de Javascript: una variable, una función, etc.

const txt = `<h1>${ title }</h1>`;

Estas expresiones también pueden ser operaciones de cálculo, sólo tienen que poder ser convertidas a cadena de texto:

const txt = `<div>Total: ${ subtotal + tax }</div>`;

Las expresiones ${ expresión } se pueden situar en cualquier parte del texto, no sólo como contenido de las etiquetas. Realmente sólo estamos manipulando cadenas de texto, por lo que también se pueden utilizar para dar valor a los atributos o para determinar que etiquetas queremos utilizar.

const txt = `<div class=${ cssClass }>My text</div>`;

También puede componer varias plantillas para crear resultados complejos. Hay varias formas de conseguir esta combinación, por ejemplo, obteniendo cadenas ya interpoladas que utilizamos en otras cadenas:

const txtName     = `<span class="${ cssName }">${ name }</span>`;
const txtLastname = `<span class="${ cssLastname }">${ lastname }</span>`;
const card        = `
    <p>${ txtName }</p>
    <p>${ txtLastname }</p>
  `;

Contenido condicional

La composición de plantillas abre varias posibilidades, incluida la utilización de expresiones condicionales por medio de los operadores ternarios. Las expresiones con el operador ternario son una excelente manera de agregar condiciones para mostrar unas u otras cadenas, que -además- pueden ser a su vez template string:

const txtLi = `
  <li>${ isLogged
    ? `Welcome ${ name }`
    : `Please log in`
  }</li>
  `;

Realmente también podríamos utilizar condicionales con sentencias if e ir concatenando las cadenas, pero no queda tan bien integradas en la plantilla como el operador ternario.

Si lo que necesitamos es una renderización condicional, es decir, que algo aparezca o no dependiendo de una condición, también podemos hacer uso del operado ternario, pero devolviendo una cadena vacía cuando queremos que no aparezca nada. Es importante no utilizar undefined o null en estos casos, ya que se convertirán en cadenas con esos nombres y no es lo que estamos buscando.

const txtLi = `
  <li>${ isLogged
    ? `Welcome ${ name }`
    : `Please log in`
  }</li>
  `;

Repetición de contenido

Es posible repetir partes de las plantillas en base a los datos que tenemos accesibles por medio de Array.map() para recorrer el contenido, junto con Array.join('') para convertir el resultado en una cadena. Esta solución es muy útil a la hora de representar listas, tablas o cualquier otro elemento que se repita en nuestro contenido en base a los datos que estamos manejando:

const txt = `
    <ul>
      ${ items.map ((item) => `<li>${ item }</li>`).join ('') }
    </ul>
  `;

Nuevamente, podríamos utilizar sentencias for o while para recorrer los datos e ir concatenando las cadenas, pero no queda tan bien integradas en la plantilla como el uso de .map().join(''). Una nota importante, si nos olvidamos de incluir el .join('') aparecerán caracteres (,) en nuestro contenido, ya que al convertir diretamente una matriz en texto, los valores son separados por una coma.

Template String y Web Components

Vamos a ver un ejemplo algo más completo de las capacidades de los template string en el contexto de un componente web:

<script>
  const path = 'https://raw.githubusercontent.com/hjnilsson/country-flags/master';
  const priv = new WeakMap ();
  
  class WCCountries extends HTMLElement {
    constructor () {
      super ();
      this.attachShadow ({mode : 'open'});
      this.shadowRoot.innerHTML = `
      <style>
        img { width: 40px; }
      </style>
      <table>
        <thead><tr><th>Flag</th><th>Country</th></tr></thead>
        <tbody></tbody>
      </table>
    `;
      priv.set (this, {data : {}});
      fetch (`${ path }/countries.json`)
        .then (response => {
          if (response.status === 200) {
            return response.json ();
          }
        })
        .then (data => {
          this.data = data;
        });
    }
    
    render () {
      const data                                        = this.data;
      this.shadowRoot.querySelector ('tbody').innerHTML = `
      ${
        Object.keys (data).map (country => `
          <tr>
            <td>
              <img src="${ path }/png100px/${ country.toLowerCase () }.png\">
            </td>
            <td>${ data[ country ] }</td>
          </tr>`).join ('')
        }
    `;
    }
    
    get data () {
      return priv.get (this).data;
    }
    
    set data (value) {
      priv.set (this, {data : value});
      this.render ();
    }
  }
  
  customElements.define ('wc-countries', WCCountries);
</script>
<wc-countries></wc-countries>

El gran problema de esta aproximación es el rendimiento. Cuando se hacen muchos cambios en los datos, se realizan muchas operaciones de renderización. Esto es debido a que cada vez que borra prácticamente todo el contenido y se vuelve a generar. Aunque los navegadores modernos han optimizado este proceso, lo cierto es que pintar el contenido en pantalla es operación es bastante pesada. Funciona perfectamente, pero los efectos de refresco de la pantalla no serán muy elegantes.

Para solventar este problema de renderización completa y la consiguiente merma de rendimiento han aparecido una serie de librerías que optimizan el proceso de creación y actualización del DOM evitando volver a crear los elementos que ya existen. La más conocida de estas librerías proviene de Google, del equipo de Polymer y se llama lit-html.

Breve introducción a Lit-html

Lit-html ofrece una gran funcionalidad, aprovecha las Template String para crear un DOM con interpolación de datos, evitando la renderización repetida cuando cambiamos los datos. Para utilizar la librería basta con instalarla con npm i lit-html y seguir unos sencillos pasos:

Funciona de forma muy sencilla:

  • vamos a trabajar con los Template String tal y como hemos descrito en la sección anterior, con todas sus características
  • incluiremos una función de plantilla html antes de los template que devolverá un objeto, en vez de una cadena
  • ese objeto debe ser pasado a la función render de la librería Lit-html y se encargará de generar el DOM de forma optimizada.

Nota: no se debe confundir Lit-html con LitElement. Lit-html es una librería bastante ligera que complementa a las Template String optimizando la creación y actualización del DOM. Por su parte, LitElement es una clase base para la construcción de componentes web, que utiliza lit-html, pero tiene mucha otra funcionalidad.

Este es un ejemplo muy sencillo, basado en uno de los primeros ejemplos de la documentación de Lit-html.

<button>Change</button>
<div id="result"></div>
<script type="module">
  import { html, render } from './node_modules/lit-html/lit-html.js';

  let sayHello = (name) => html`<h1>Hello ${ name }</h1>`;
  render (sayHello ('World'), document.querySelector('#result'));

  document.querySelector('button').addEventListener('click', () => {
    render (sayHello ('Everyone'), document.querySelector('#result'));
  });
</script>

Lo cierto es que lit-html ofrece algunas cosas más, pero con lo que hemos visto podemos adaptar el código que habíamos mostrado anteriormente con template string para que funcionen con esta librería. En este ejemplo hemos utilizado lit-html para mostrar la lista de banderas y paises.

<script type="module">
  import { html, render } from './node_modules/lit-html/lit-html.js';
  
  const path = 'https://raw.githubusercontent.com/hjnilsson/country-flags/master';
  const priv = new WeakMap ();
  
  class WCCountries extends HTMLElement {
    constructor () {
      super ();
      this.attachShadow ({mode : 'open'});
      priv.set (this, {data : {}});
      fetch (`${ path }/countries.json`)
        .then (response => {
          if (response.status === 200) {
            return response.json ();
          }
        })
        .then (data => {
          this.data = data;
        });
    }
    
    render () {
      const data = this.data;
      render (html`
      <style>
        img { width: 40px; }
      </style>
      <table>
        <thead><tr><th>Flag</th><th>Country</th></tr></thead>
        <tbody>
          ${ Object.keys (data).map (country => html`
          <tr>
            <td>
              <img src="${ path }/png100px/${ country.toLowerCase () }.png\">
            </td>
            <td>
              ${ data[ country ] }
            </td>
          </tr>`) }
        </tbody>
      </table>
    `, this.shadowRoot);
    }
    
    get data () {
      return priv.get (this).data;
    }
    
    set data (value) {
      priv.set (this, {data : value});
      this.render ();
    }
  }
  
  customElements.define ('wc-countries', WCCountries);
</script>
<wc-countries></wc-countries>

La librería Lit-html es de gran ayuda si trabajamos con Template String en proyectos reales, donde los datos se actualizan con frecuencia, y donde queremos evitar que el usuario observe molestos refrescos de la pantalla. Ofrece algunas cosas adicionales a las que acabamos de describir, pero con lo que hemos aprendido ya podemos sacarle mucho partido.

Conclusiones

Comentábamos al principio de este artículo que seguramente nos parecería algo limitada la funcionalidad que nos ofrecen los templates, tanto el elemento template del HTML como los Template String. Lo cierto es que cuando se conocen en profundidad se les puede sacar mucho partido y conseguir que generar el contenido de nuestros componentes con bastante flexibilidad.

Aunque puede parecer poco interesante, la capacidad de templace para crear fragmentos de DOM ya interpretados por el navegador para ser clonados, adaptados e insertados cuando sea necesario, es una funcionalidad que nos puede ser de utilidad en muchas ocasiones. Es cierto que la dificultad para crear estas plantillas de forma independiente e importarlas a nuestro proyecto hacen que sean poco efectivas, pero es posible que esta limitación se resuelva en un futuro próximo.

Junto con librerías como Lit-html que optimizan el proceso de creación del DOM, las capacidades compositivas y de interpolación de las template string son bastante completas y se pueden utilizar sin problemas en proyectos complejos. Es necesario adaptarse un poco a su peculiar forma de trabajar, utilizando un modelo de programación más próximo al funcional, para ajustarnos a las capacidades de las expresiones que pueden contener estas plantillas.

Novedades

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.

En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.

Observables en Javascript con Proxies

Observables en Javascript con Proxies

En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.