Holi, soy Nuzkito

y me dedico al desarrollo de software

Plantillas en JavaScript con ES2015

Una de las características añadidas a JavaScript con ES2015 fueron los template literals, la posibilidad de tener cadenas de texto que se comportan como plantillas. Esta funcionalidad nos permite incluir expresiones dentro de cadenas de texto, haciendo que concatenar strings sea mucho más sencillo y legible.

Tradicionalmente en JavaScript podemos crear cadenas de texto tanto con comillas simples como con comillas dobles. No hay ninguna diferencia entre usar una u otra opción. Para usar un string como plantilla se ha incluido una tercera, usando el caracter `.

const language = 'JavaScript';
const message = `<h1>${language}</h1>`;
console.log(message); // <h1>JavaScript</h1>

En este ejemplo, la variable language es un string normal, mientras que message es una plantilla. La forma de introducir expresiones dentro de una plantilla es con el símbolo del dolar y las llaves ${}. En este caso se está concatenando una variable, pero podemos incluir cualquier otra expresión, como una llamada a una función.

Fíjate que para hacer la misma concatenación con strings normales, el código se vería así:

const message = '<h1>' + language + '</h1>';

Según se va ganando complejidad, la legibilidad empeora.

const n1 = 7;
const n2 = 3;
// ES5
console.log('La suma de ' + n1 + ' + ' + n2 ' es ' + (n1 + n2) + '.');
// ES6
console.log(`La suma de ${n1} + ${n2} es ${n1 + n2}`);

Modifica el comportamiento de las plantillas

Por defecto, las plantillas envían las cadenas literales y los resultados de las expresiones a una función, que concatena todos los valores y devuelve el resultado.

Podemos crear una función que modifique dicho comportamiento. Para llamarla hay que poner el nombre delante del primer `.

function template() {
    return 'Hola mundo';
}

console.log(template`Hello world`); // Hola mundo

A estas funciones se las llama tags(etiquetas). Esta función recibe como primer parámetro todos los literales que tenga la plantilla, y en los sucesivos parámetros el resultado de las expresiones.

function template(strings, hello, world) {
    console.log(strings);
    console.log(hello);
    console.log(world);
}

const hello = 'Hola';
const world = 'mundo';
template`<h1>${hello} ${world}</h1>`;
// [ '<h1>', ' ', '</h1>' ]
// Hola
// mundo

Como las expresiones de cada plantilla pueden variar, se puede usar el operador ... para recoger todos los valores en un array.

function template(strings, ...values) {
    console.log(strings);
    console.log(values);
}

Podemos reproducir la funcionalidad por defecto de esta forma, por ejemplo.

function template(strings, ...values) {
    let result = '';

    for (let i = 0; i < strings.length; i += 1) {
        result += strings[i];
        result += values[i] || '';
    }

    return result;
}

Plantillas complejas

Si queremos crear componentes más complejos, que puedan por ejemplo iterar sobre una lista de elementos, es posible sin ningún problema usar alguna función como map para mostrar dichos elementos.

function render(title, items) {
    return `
        <h1>${title}</h1>
        <ul>
            ${items.map(item => `<li>${item}</li>`)}
        </ul>
    `
}

console.log(render('Lenguajes', ['HTML', 'CSS', 'JS']));

Esto funciona, pero tenemos un pequeño problema. El método map devuelve un array, y cuando se transforma un array a string, se incluye una coma entre cada elemento.

// <li>HTML</li>,<li>CSS</li>,<li>JS</li>

Esto se puede solucionar usando el método join tras map.

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

Pero eso nos obliga a tener lógica adicional en la plantilla. Podemos abstraer esta lógica creando una etiqueta que modifique el comportamiento al momento de concatenar un array.

Partamos de la función template creada anteriormente.

function template(strings, ...values) {
    let result = '';

    for (let i = 0; i < strings.length; i += 1) {
        result += strings[i];
        result += values[i] || '';
    }

    return result;
}

Aunque antes voy a refactorizar un poco el código.

function getPieces(strings, values) {
    return strings.map(function(string, i) {
        return [string, values[i]];
    }).reduce(function(accumulator, item) {
        return [...accumulator, ...item];
    }, []).filter(a => a);
}

function template(strings, ...values) {
    return getPieces(strings, values).join('');
}

La función getPieces devolverá un array con todas las piezas a concatenar en orden. De esta forma será más sencillo agregar modificaciones. Usando el método join sobre el resultado de getPieces se obtiene el mismo resultado que teníamos al principio.

Pero lo que queríamos era que, si en la plantilla teníamos algún array, este se concatenase sin las comas. Para ello antes de hacer el join tenemos que hacer un map que compruebe si el valor es un array, y en dicho caso, haga un join sobre él.

function template(strings, ...values) {
    return getPieces(strings, values).map(function(item) {
        return Array.isArray(item) ? item.join('') : item;
    }).join('');
}

Fácil, ¿no?. Ahora ya podemos usar map en las plantillas sin necesidad de hacer un join.

function render(title, items) {
    return template`
        <h1>${title}</h1>
        <ul>
            ${items.map(item => `<li>${item}</li>`)}
        </ul>
    `
}

De la misma manera podrían agregarse más funcionalidades, como protección contra vulnerabilidades de XSS en caso de que usemos las plantillas para crear HTML, como en los ejemplos.

Más información