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

Datos inmutables en Javascript

Datos inmutables en Javascript

En Javascript todo parece mutable, es decir, que se puede cambiar, pero lo cierto es que también nos ofrece varios mecanismos para conseguir que los datos que manejamos, especialmente los objetos, sean inmutables. Te invitamos a descubrir cómo…

Copiar objetos en Javascript

Copiar objetos en Javascript

Copiar objetos no es algo sencillo, incluso se podría decir que en si mismo no es posible, ya que el concepto «copiar» no entra dentro del paradigma de los objetos. No obstante, por medio de instrucciones como Object.assign() hemos aprendido como obtener objetos con las mismas propiedades, pero está técnica no se puede aplicar a todos los tipos de objetos disponibles en Javascript. Vamos a ver cómo podemos copiar cualquier tipo de objeto…

Descubre los Javascript Array Internals

Descubre los Javascript Array Internals

El Array es una de las estructuras más utilizadas en Javascript y no siempre bien comprendida. Hoy os invitamos a analizar el comportamiento interno de este objeto y descubrir cómo Javascript implementa las diferente acciones con los Array y que operaciones internas se realizan en cada caso.

Web Components: pasado, presente y futuro

Web Components: pasado, presente y futuro

Los Web Components aparecieron en el panorama de desarrollo hace ya bastante tiempo. Desde su presentación se les ha prestado mucha atención, pero lo cierto es que no han sido acogidos de forma generalizada, quizás por la difusión de nuevos y potentes frameworks. Nos preguntamos qué ha pasado con este estándar y, sobre todo, que puede pasar de aquí en adelante con el uso práctico de los componentes web.