Template Strings en ES6: estudiando las nuevas Plantillas de Cadena en Javascript

05 Oct 2016

Introducción

Javascript ha ido evolucionando a pasos agigantados a lo largo de la última década siendo a día de hoy una parte esencial de la tecnología web. En este camino del éxito, ha sido a menudo necesario complementar la flexibilidad nativa del lenguaje con bibliotecas de terceros que hicieran más sencillo algunas funcionalidades básicas.

Una de estas necesidades recurrentes ha sido la gestionar un sistema de plantillas de texto que permitan separar las capas de lógica y representación, permitiendo con ello aislar el marcado Javascript del HTML sobre el que se despliega.

Con el nuevo estándar ES6 se han introducido las Plantillas de Texto de forma nativa permitiendo con ello cubrir, al menos parcialmente, una necesidad habitual del desarrollo moderno. Pasemos a estudiarlas con detalle.

Plantillas de Texto

Las plantillas de texto (o Template Strings) son cadenas literales de texto incrustadas en el código fuente que permiten su interpolación mediante expresiones. Históricamente, el nombre que ha recibido esta estructura es la de Quasi Litarals (‘casi literales’).

Su sintaxis es sencilla y consta de dos aspectos fundamentales:

  • Se utilizan las comillas invertidas (`) para delimitar las cadenas
  • Para añadir marcadores de posición (o placeholders) utilizamos el signo de dolar ($) seguido de llaves
var myTemplateString = 'La donna e mobile cual piuma al vento';
 
console.info( `String value: ${ myTemplateString }` );
// String value: La donna e mobile cual piuma al vento

Hay que notar que la cadena natural se encierra entre las comillas habituales (simples o dobles) mientras que cuando queremos utilizar la plantilla, lo hacemos con las comillas invertidas (cuasi literal). De forma complementaria, la estructura ${} habilita la interpolación sustituyendo el fragmento por el valor al que apunta.

¿Comillas invertidas?

El exótico uso de las comillas invertidas tiene como objetivo salvaguardar la retrocompatibilidad en códigos anteriores que ya usan las comillas tradicionales (simples y/o dobles). Es por ello que durante su especificación, se recurrió a un sistema auxiliar de entrecomillado que tampoco es ajeno a estas estructuras.

En un sistema tradicional de plantillas de texto, una expresión cuasi-literal (o cuasi-expresión) consiste en una función opcional auxiliar de postproceso (quasi-parser), un cuasi-entrecomillado (quasi-quotation o como comilla invertida «`») y una cadena válida para la función de postprocesado.

Multilínea!

Gracias a las plantillas, ahora es posible disfrutar de cadenas multilínea en Javascript sin necesidad de recurrir a concatenaciones, barras invertidas, unión de arrays u otros malabares habituales:

var myTemplateString = `La donna e mobile
cual piuma al vento`;
 
console.info( myTemplateString );
// La donna e mobile
// cual piuma al vento

NOTA: Es importante recalcar que las plantillas mantienen el formato introducido, incluyéndose los saltos de línea y las tabulaciones:

var myTemplateString = `La donna e mobile
    cual piuma al vento`;
 
console.info( myTemplateString );
// La donna e mobile
//     cual piuma al vento

Interpolación de cadenas

La interpolación permite sustituir marcadores de posición (placeholders) en tiempo de ejecución por sus correspondientes valores a través de expresiones. Cualquier expresión válida en el lenguaje lo es en la plantilla:

Arrays

var arr = [ 'la', 'donna', 'e', 'mobile' ];
 
console.info( `My chain joined: ${ arr.join( ' ' ) }` );
// My chain joined: la donna e mobile

Objetos

// Objects
var collection = [
    {
        id: 1,
        title: 'The Neverending Story'
    }, {
        id: 2,
        title: 'Momo'
    }
];
 
var myID = 2;
 
console.info( `Book title: ${ collection.find( item => item.id === myID ).title }` );
// Book title: Momo

Métodos

// Methods
var str = 'la donna e mobile';
 
console.info( `Uppercase: ${ str.toUpperCase() }` );
// Uppercase: LA DONNA E MOBILE

Matemáticas

var val1 = 100,
    val2 = 200;
 
console.info( `The sum: ${ val1 + val2 }` ); // 300

Operador ternario

var book = {
    id: 1,
    title: false,
    year: 2016
};
 
console.info( `Book report: ${ book.title ? book.title : 'Missing book title!' }` );
// Book report: Missing book title!

Operadores lógicos

var book = {
    id: 1,
    title: 'The Neverending Story',
    year: false
};
 
console.info( `Book year: ${ book.year || 'Unkonwn' }` );
// Book year: Unknown

Indefiniciones y coherción

En caso de que un valor no esté definido, o no se correspondan sus tipos, las expresiones se evalúan de la forma habitual a como el motor Javascript lo haría fuera de la plantilla.

var val1 = 100,
    val2 = '200';
 
console.info( `The sum: ${ val1 + val2 }` ); // 100200
 
var val3 = 100,
    val4 = undefined;
 
console.info( `The sum: ${ val3 + val4 }` ); // NaN

Postprocesado de plantillas

Hasta ahora, hemos utilizado las plantillas en su forma simple, pero podemos ir un paso más allá cuando utilizamos una función para postprocesar una entrada: es lo que conocemos como una función de tipo quasi-parser. La idea es transformar la salida de la plantilla de modo que se adecue a nuestras necesidades.

La sintaxis y su uso no resultan intuitivos, por lo que los iremos viendo de forma aislada y simplificada.

var tag = strings => console.log(strings);
 
tag`Hello World`;
// [ "Hello World" ]

Hemos creado una función llamada ‘tag‘ (quasi-parser) que recoge un parámetro ‘strings‘ y que como salida pinta ese mismo valor. A continuación, llamamos a esa función pero, en lugar de utilizar los paréntesis habituales, lo hacemos con una plantilla de texto (quasi-quotation).

El resultado, es que ‘tag‘ recibe la plantilla como argumento y nos la pinta en pantalla (nótese que devuelve un array). Podemos desarrollar el ejemplo anterior (suprimiendo temporalmente la función flecha) para aclarar un poco:

var tag = function ( strings ) {
    return strings;
};
 
var result = tag`Hello World`;
 
console.info( result ); // [ "Hello World" ]

De ese modo, nuestra función recoge los literales pero ¿y la interpolación? Pues la conseguimos a través del resto de parámetros:

var tag = function ( strings, ...values ) {
    console.info( strings );
    console.info( values );
};
 
var name = 'Charles',
    city = 'Planilandia';
 
tag`Hello ${ name }, Welcome to ${ city }!`;
// [ "Hello ", ", Welcome to ", "!" ]
// [ "Charles", "Planilandia" ]

Hemos añadido un nuevo argumento a nuestra función ‘tag‘, en realidad hemos agrupado todos los que lleguen a partir del segundo usando el operador arrastre; y ese array está recogiendo los valores obtenidos de la interpolación.

Es muy cuestionable que en lugar de un único argumento donde se recogiesen todos los valores, éstos lleguen de forma sucesiva haciendo necesario el uso del operador de arrastre, pero es así… La sintaxis sería como sigue:

function tag( [ param[ ,param[ , ...param ] ] ] )

Básicamente, es la misma sintaxis de una función ordinaria.

Sacando partido al postprocesado

Vista la teoría del postproceso, vamos a mostrar un par de ejemplos donde buscamos sacarle un sentido práctico.

Marcado HTML5 para imágenes

Un primer caso sencillo sería crear la plantilla correcta para pintar el marcado HTML de una imagen:

function templater ( strings, ...keys ) {
    return function( data ) {
        let temp = strings.slice();
 
        keys.forEach( ( key, i ) => {
            temp[ i ] = temp[ i ] + data[ key ];
        } );
 
        return temp.join( '' );
    }
};
 
var img1 = {
    name: 'Hidetaka Miyazaki',
    src: 'Hidetaka_miyazaki.jpg',
    caption: 'Japanese God'
};
 
var imgTemplate = templater`<figure>
    <img alt="${ 'name' }" src="${ 'src' }">
    <figcaption>${ 'caption' }</figcaption>
</figure>`;
 
var myTemplate = imgTemplate( img1 );
console.info( myTemplate );
 
// <figure>
//      <img alt="Hidetaka Miyazaki" src="Hidetaka_miyazaki.jpg">
//      <figcaption>Japanese God</figcaption>
// </figure>

Si omitimos por un momento la función ‘templater‘, el resto del código es autoexplicativo: hemos creado una plantilla con el marcado HTML de una imagen para más adelante rellenarlo a partir de un objeto dado.

La magia se cocina precisamente en esa función ‘templater‘, donde primero creamos una copia de los literales y a continuación iteramos sobre los valores a interpolar para ir construyendo con ellos la nueva salida. Finalmente el array obtenido se convierte en una cadena y se devuelve al intérprete.

Si queremos explotar un array de imágenes para así obtener todo el marcado necesario, utilizaríamos algo similar a esto:

var pics = [
    {
        name: 'Hidetaka Miyazaki',
        src: 'Hidetaka_miyazaki.jpg',
        caption: 'Japanese God'
    }, {
        name: 'Hironobu Sakaguchi',
        src: 'Hironobu_sakaguchi.jpg',
        caption: 'Another Japanese God'
    }
];
 
var myTemplate = pics.map( pic => imgTemplate( pic ) ).join( '\n' );
console.info( myTemplate );
 
// <figure>
//     <img alt="Hidetaka Miyazaki" src="Hidetaka_miyazaki.jpg">
//     <figcaption>Japanese God</figcaption>
// </figure>
// <figure>
//     <img alt="Hironobu Sakaguchi" src="Hironobu_sakaguchi.jpg">
//     <figcaption>Another Japanese God</figcaption>
// </figure>

La cosa no pinta mal y comenzamos a verle a esto usos interesantes…

Creación dinámica de tablas

Este ejemplo es más complejo y utiliza otra aproximación diferente a la anterior. De hecho, la base sería reutilizable para emular un sistema de plantillas clásico a los que estamos más habituados. La idea parte de Claus Reinke y fue desarrollada en 2ality.

Empezamos con un marcado semántico en el que definimos nuestra tabla base:

var tmpl = users => html`
    <table>
        ${ users.map( user => html`
            <tr>
                <td>$${ user.id }</td>
                <td>$${ user.name }</td>
                <td>$${ user.email }</td>
                <td>$${ user.role }</td>
            </tr>
        ` ) }
    </table>
`;

Aquí estamos utilizado una expresión dentro de nuestra plantilla. Como la idea es iterar sobre una colección para rellenar nuestra tabla, utilizamos el método map del objeto Array.

Si observamos el callback para cada elemento, vemos que llamamos de nuevo a la función html (la función auxiliar de postproceso ‘quasi-parser‘ que veremos a continuación). El doble signo de dolar ($$) no es parte de la sintaxis ES6, sino solo un arreglo que utilizaremos a continuación para indicarle a nuestro motor de plantillas que queremos escapar caracteres durante las sustituciones.

La función ‘html‘ quedaría como sigue (comentada en el propio código):

function html ( literalSections, ...substs ) {
    // Usamos el literal crudo para no interpretar
    // caracteres raros.
    let raw = literalSections.raw;
 
    let result = '';
 
    substs.forEach( ( subst, i ) => {
        // Almacenamos el literal que precede a la
        // sustitución actual
        let lit = raw[ i ];
 
        // En el ejemplo, map() devuelve un array:
        // Si la sustitución es un array (y no una cadena),
        // la convertimos.
        if ( Array.isArray( subst ) ) {
            subst = subst.join( '' );
        }
 
        // Si la sustitución está precedida de un signo de dolar,
        // escapamos caracteres (comillas, saltos de línea...).
        // Previamente comprobamos que se trate de una cadena y
        // no de un número.
        if ( lit.endsWith( '$' ) ) {
            subst = isNaN( subst ) ? htmlEscape( subst ) : subst;
            lit = lit.slice( 0, -1 );
        }
        result += lit;
        result += subst;
    } );
 
    // Eliminamos el último literal, el cual siempre es una
    // cadena vacía.
    result += raw[ raw.length - 1 ];
 
    return result;
}

Para este fragmento, necesitamos el apoyo de una pequeña función que escape caracteres peligrosos. Eso nos salvaguarda de los errores que podrían producir en la salida una entrada que incluyese ya algo de marcado y, por tanto, susceptible de romper el HTML de nuestra tabla:

function htmlEscape ( str ) {
    return str.replace( /&/g, '&' )
        .replace( />/g, '>' )
        .replace( /</g, '<' )
        .replace( /"/g, '"' )
        .replace( /'/g, '&#39;' )
        .replace( /`/g, '&#96;' );
}

Ahora tomamos un conjunto de usuarios:

var users = [
    {
        id: 1,
        name: 'Yasumi Matsuno',
        email: 'yasumi_matsuno@example.com',
        role: 'director'
    }, {
        id: 2,
        name: '<Hiroshi Minagawa>',
        email: 'hiroshi_minagawa@example.com',
        role: 'artist'
    }, {
        id: 3,
        name: 'Akihiko Yoshida',
        email: 'akihiko_yoshida@example.com',
        role: 'artist'
    }
];

NOTA: En el nombre del item 2, hemos incluido un par de llaves para observar cómo nuestra función auxiliar de escape las transforma salvando así el HTML de salida.

Comprobamos finalmente el funcionamiento de nuestro código ejecutando la plantilla:

console.info( tmpl( users ) );
 
// <table>
//     <tr>
//         <td>1</td>
//         <td>Yasumi Matsuno</td>
//         <td>yasumi_matsuno@example.com</td>
//         <td>director</td>
//     </tr>
//
//     <tr>
//         <td>2</td>
//         <td><Hiroshi Minagawa></td>
//         <td>hiroshi_minagawa@example.com</td>
//         <td>artist</td>
//     </tr>
//
//     <tr>
//         <td>3</td>
//         <td>Akihiko Yoshida</td>
//         <td>akihiko_yoshida@example.com</td>
//         <td>artist</td>
//     </tr>
//
// </table>

El resultado es el esperado: la tabla se construye con arreglo a la colección de usuarios e incluso escapa los caracteres que pudieran romper el marcado.

Las plantillas nativas frente a soluciones de terceros

El mundo de las plantillas en Javascript no es nuevo; de hecho, ya hemos comentado como su implementación nativa responde a una necesidad real que obligaba hasta ahora a depender de soluciones externas. Handlebars, HoganJS, Mustache… hay muchas. Eso nos lleva a un par de preguntas muy razonables: ¿son aún necesarias estas bibliotecas? ¿por qué escribir ahora mis propias funciones de postproceso si otras bibliotecas ya lo hacen de forma óptima?

¿Son aún útiles las bibliotecas de plantillas?

Sí; y por varias razones: la principal es que las primeras implementaciones de métodos nativos siempre son limitadas. Por el momento, las bibliotecas de terceros cubren aspectos más complejos que podemos necesitar en escenarios complejos. Por otro lado, como tecnología experimental, no hay que perder de vista el soporte actual entre los diferentes navegadores, algo menor que veremos más adelante en este mismo post.

Podemos concluir sobre este punto que, dependiendo de nuestras necesidades, tendremos que recurrir o no a herramientas externas más complejas. Si tras analizar los ejemplos anteriores se tienen dudas sobre cómo implementar las plantillas en nuestro sistema, probablemente estemos ante un escenario más complejo que el ideal para
esta nueva funcionalidad.

¿Por qué tendría ahora que escribir desde cero mis propias funciones?

Esta respuesta ya es completamente subjetiva: podemos querer tener el control absoluto de nuestro código (aún a riesgo de cometer algún error o quedarnos cortos en sus funcionalidades). También es posible que nuestras necesidades sean muy concretas y una biblioteca al uso no las aborde; o simplemente queremos desarrollar un software propietario y las licencias libres de esas famosas herramientas nos lo impiden…

Aquí de nuevo el sentido común, o la línea de negocio, la que tiene que marcarnos las pautas y ayudarnos a escoger la mejor opción para nuestro caso particular.

Soporte de las Plantillas de Cadena

A fecha de redacción de este artículo (octubre, 2016), el soporte actual es casi completo: Edge (v13, v14), Firefox (v45-v52), Chrome (v53-v55) y Safari (9-TP) lo soportan, así como los compiladores más utilizados (Traceur, Babel, Closure y TypeScript). El soporte en Node también es completo.

support

Rendimiento nativo

Lamentablemente, no puedo realizar pruebas complejas de rendimiento al encontrarse desde hace un tiempo offline la plataforma JsPerf.

Cuando esta gran herramienta vuelva a estar operativa, actualizaré el post con los resultados comparados.

Para saber más:

Draft ECMA-262
September 29, 2016
ECMAScript® 2017 Language Specification
https://tc39.github.io/ecma262/#sec-template-literals

Más:

{3} Comentarios.

  1. Santiago Goitisolo

    Buen aporte, definitivamente habrá que echarle un vistazo a estas plantillas.

    Gracias!

  2. Jorge

    Hola, muy buenos tus aportes. Estoy un poco confundido al ver que utilizas «let» en vez de «var» dentro de una función, cuando aparentemente no es necesario, ya que las closures las marcan las funciones, como habías explicado en otro artículo. Hay quienes están a favoy y en contra con respecto a la utilización de «let» por defecto siempre que no se quiera un comportamiento contrario. A ver si un día nos explicas los usos más convenientes de ambos. Hay un articulo en inglés que habla al respecto, pero como está en inglés cuesta un poco seguirlo: https://davidwalsh.name/for-and-against-let

    Saludos y gracias.

    • Carlos Benítez

      Hola Jorge;
      sí… es un tema que da pie a debate.

      Las recomendaciones son utilizar siempre ‘let’ y ‘const’ en lugar de ‘var’. La razón es que de esta forma, se contextualiza el ámbito de las variables. Tienes razón cuando comentas que sería la propia función la que marca la clausura, pero aún así, se prefiere la opción ‘let’ por ser más precisa. En general, el uso de ‘var’ debería ser anecdótico, dejándolo solo a casos donde no sepamos con exactitud el ámbito o contexto de la variable que estamos declarando.

      Sin embargo, en este blog verás casi exclusivamente el uso de ‘var’ en los códigos… ¿Por qué lo hago si va contra las recomendaciones? Pues ya lo comenté en algún otro artículo: para que podáis jugar con los ejemplos fácilmente en la consola del navegador usando un rápido copia/pega. En esos casos, aunque ‘let’ no daría problemas, sí lo hace ‘const’ (no podrías ejecutar el código dos veces sin recargar la página donde esté abierta la consola). Es por eso que decidí utilizar exclusivamente ‘var’ en el blog, comentando de vez en cuando como ahora que lo ideal sería reemplazar la declaración de variables según convenga en cada caso.

      Me apunto la recomendación de hablar más sobre el tema 🙂
      Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *