Perspectivas sobre el cliente

Características avanzadas de plantillas de JsRender

John Papa

Descargar el ejemplo de código

John PapaLas plantillas son poderosas, pero algunas veces necesitamos algo más que las características estándar ofrecidas por un motor de plantillas sin hacer nada más. Tal vez deseemos convertir datos, definir funciones auxiliares personalizadas o crear una etiqueta propia. Lo bueno es que las características principales de JsRender nos permiten hacer todo esto y mucho más.

En mi artículo de abril (msdn.microsoft.com/magazine/hh882454) exploré las características fundamentales de la biblioteca de plantillas de JsRender. En este artículo sigo explorando JsRender en situaciones nuevas, como por ejemplo la representación de plantillas externas, el cambio del contexto con la etiqueta {{for}} y el uso de expresiones complejas. También mostraré cómo usar algunas de las características más poderosas de JsRender, como la creación de etiquetas, convertidores y funciones auxiliares contextuales personalizados y el uso de código personalizado. Todos los ejemplos de código se pueden descargar desde archive.msdn.microsoft.com/mag201205ClientInsight y JsRender se puede descargar desde bit.ly/ywSoNu.

Variaciones {{for}}

Hay varias formas en las que la etiqueta {{for}} puede ser la solución ideal. En mi artículo anterior mostré cómo la etiqueta {{for}} permite iterar a través de varios conjuntos con el uso de un bloque y cómo puede iterar a través de varios objetos simultáneamente: 

    

<!-- looping {{for}} -->
{{for students}}
{{/for}}           
<!--  combo iterators {{for}} -->
{{for teachers students staff}}
{{/for}}

La etiqueta {{for}} (o cualquier etiqueta de bloque) se puede convertir desde una etiqueta de bloque (con contenido) a una etiqueta de cierre automático al reemplazar el contenido del bloque con una plantilla externa, la que apunta de manera declarativa como una propiedad tmpl. Luego la etiqueta presentará la plantilla externa en lugar del contenido en línea.

Esto facilita la adopción de un enfoque modular para las plantillas, el que nos permite reutilizar el marcado de las plantillas en diferentes lugares y organizar y componer las plantillas:

<!--  self closing {{for}} -->
{{for lineItems tmpl="#lineItemsDetailTmpl" /}}

Los datos rara vez son planos, razón por la cual una de las características importantes de las plantillas es que permitan adentrarse y salir de las jerarquías de objetos. En mi artículo anterior, expuse las técnicas principales para adentrarse en la jerarquía de objetos con el uso de la notación de puntos y corchetes, pero también podemos usar la etiqueta {{for}} para reducir el código. Esto se hace más evidente cuando tenemos una estructura de objeto en la que nos adentramos en una jerarquía de objeto y tenemos que representar un conjunto de propiedades de un objeto secundario. Por ejemplo, al representar la dirección de un objeto de persona, tal vez escribamos la plantilla de la siguiente forma, donde el término “address” en la ruta se repite varias veces:

    <div>{{:address.street1}}</div>
    <div>{{:address.street2}}</div>
    <div>{{:address.city}}, {{:address.state}} {{:address.postalCode}}</div>

La etiqueta {{for}} permite simplificar el código para representar la dirección, al eliminar la necesidad de repetir el objeto address, tal como se aprecia aquí:

    <!--  "with" {{for}} -->
    {{for address}}
      <div>{{:street1}}</div>
      <div>{{:street2}}</div>
      <div>{{:city}}, {{:state}} {{:postalCode}}</div>
    {{/for}}

La etiqueta {{for}} opera en la propiedad address, que es un objeto singular con propiedades, no un conjunto de objetos. Si la dirección es verdadera (si contiene algunos valores que no son falsos), entonces se representará el contenido del bloque {{for}}. La etiqueta {{for}} también cambia el contexto actual de datos del objeto de persona al objeto de dirección, por lo que actúa como el comando “with” que poseen muchas bibliotecas y lenguajes. De manera que en el ejemplo anterior, la etiqueta {{for}} cambia el contexto de datos a address y luego representa el contenido de las plantillas una vez (ya que solo hay una dirección). Si la persona no tiene una dirección (la propiedad de dirección es nula o no está definida), no se representará ningún contenido. Por esta razón, el bloque {{for}} es muy útil para contener plantillas que solo se deben mostrar en ciertas circunstancias. Este ejemplo (del archivo 08-for-variations.html en la descarga de código adjunta) ilustra cómo el ejemplo usa la etiqueta {{for}} para mostrar información de precios, cuando esta existe:

    {{for pricing}}
      <div class="text">${{:salePrice}}</div>
      {{if fullPrice !== salePrice}}
        <div class="text highlightText">PRICED TO SELL!</div>
      {{/if}}
    {{/for}}

Plantillas externas

La reutilización de código es una de las mayores ventajas del uso de plantillas. Si una plantilla se define dentro de una etiqueta <script> en la misma página en que se usa, entonces la plantilla no será tan reutilizable como podría serlo. Las plantillas que deben ser accesibles desde varias páginas se pueden crear en archivos independientes y recuperar cuando sean necesarias. JavaScript y jQuery facilitan la recuperación de una plantilla desde un archivo externo y JsRender facilita su representación. 

Una convención que me gusta usar con las plantillas externas es usar un prefijo con un guion bajo en el nombre del archivo, lo que corresponde a una convención común en las vistas parciales. También suelo aplicar el sufijo .tmpl.html a todos los archivos de plantillas. El sufijo .tmpl denota que se trata de una plantilla y la extensión .html simplemente permite que las herramientas de desarrollo como Visual Studio reconozcan fácilmente que la plantilla contiene HTML. En la Figura 1 podemos observar la representación de una plantilla externa.

Figura 1 Código para representar una plantilla externa

my.utils = (function () {
  var
    formatTemplatePath = function (name) {
      return "/templates/_" + name + ".tmpl.html";
    },
    renderTemplate = function (tmplName, targetSelector, data) {
      var file = formatTemplatePath(tmplName);
      $.get(file, null, function (template) {
        var tmpl = $.templates(template);
        var htmlString = tmpl.render(data);
        if (targetSelector) {
          $(targetSelector).html(htmlString);
        }
        return htmlString;
          });
        };
    return {
      formatTemplatePath: formatTemplatePath,
        renderExternalTemplate: renderTemplate
    };
})()

Una forma de recuperar una plantilla desde un archivo externo es escribir una función de utilidad que podrá llamar el JavaScript en la aplicación web. Observe en la Figura 1 que la función renderExternalTemplate del objeto my.utils primero recupera la plantilla, mediante la función $.get. Cuando se finaliza la llamada, la plantilla JsRender se crea con la función $.templates a partir del contenido de la respuesta. Finalmente, la plantilla se representa mediante la función de representación de la plantilla y el HTML resultante se muestra en el objetivo. Este código se puede llamar con el siguiente código, en el que el nombre de la plantilla, el objetivo DOM y el contexto de los datos se pasan a la función personalizada renderExternalTemplates:

my.utils.renderExternalTemplate("medMovie", "#movieContainer", my.vm);

La plantilla externa de este ejemplo es el archivo de ejemplo _medMo­vie.tm­pl.html y contiene solo las etiquetas HTML y JsRender. Está encapsulado en una etiqueta <script>. Prefiero esta técnica para las plantillas externas, ya que el entorno de desarrollo reconocerá que los contenidos son HTML, y así tendré menos posibilidades de cometer errores durante la escritura, ya que IntelliSense funcionará automáticamente. Pero también es posible que el archivo contenga varias plantillas, cada una encapsulada dentro de una etiqueta <script> y que cada una posea un identificador único. Esta es simplemente otra forma de administrar las plantillas externas. En la Figura 2 podemos apreciar el resultado final.

The Result of Rendering an External Template
Figura 2 Resultado de la representación de una plantilla externa

Rutas de visualización

JsRender proporciona varias rutas de visualización especiales que facilitan el acceso al objeto de la vista. #view proporciona acceso a la vista actual, #data proporciona acceso al contexto de datos actual de la vista, #parent asciende por la jerarquía de objetos e #index devuelve una propiedad de índice:

    <div>{{:#data.section}}</div>
    <div>{{:#parent.parent.data.number}}</div>
    <div>{{:#parent.parent.parent.parent.data.name}}</div>
    <div>{{:#view.data.section}}</div>

Al usar las rutas de visualización (aparte de #view), estas ya operan en la vista actual. En otras palabras, las siguientes dos son equivalentes:

#data
#view.data

Las rutas de visualización son útiles a la hora de navegar jerarquías de objetos, como por ejemplo clientes con pedidos con detalles del pedido, o películas en una bodega en ubicaciones de almacenamiento (como se muestra en el archivo de ejemplo 11-view-paths.html).

Expresiones

Las expresiones comunes son una parte fundamental de la lógica y pueden resultar útiles al decidir cómo representar una plantilla. JsRender reconoce las expresiones comunes que aparecen en la Figura 3, entre otras.

Figura 3 Expresiones comunes en JsRender

Expresión Ejemplo Comentarios
+ {{ :a + b }} Adición
- {{ :a - b }} Sustracción
* {{ :a * b }} Multiplicación
/ {{ :a / b }} División
|| {{ :a || b }} O lógico
&& {{ :a && b }} Y lógico
! {{ :!a }} Negación
? : {{ :a === 1 ? b * 2: c * 2 }} Expresión ternaria
( ) {{ :(a||-1) + (b||-1) }} Evaluación de ordenamiento con paréntesis
% {{ :a % b }} Operación de módulo
<= y >= y < y > {{ :a <= b }} Operaciones de comparación
=== y !== {{ :a === b }} Igualdad y diferencia

JsRender permite la evaluación de expresiones, pero no la asignación de la expresión, ni la ejecución de código al azar. Esto impide las expresiones que de otra forma podrían realizar asignaciones de variables o realizar operaciones como abrir una ventana de alerta. La intención de las expresiones es evaluar una expresión y representar el resultado, realizar una acción a partir del resultado o usar el resultado en otra operación.

Por ejemplo, al realizar {{:a++}} con JsRender se generaría un error, ya que intenta incrementar la variable a. Además, realizar {{:alert(‘hello’)}} genera un error ya que intenta llamar una función, #view.data.alert, la cual no existe.

Registro de etiquetas personalizadas

JsRender ofrece varios puntos de extensibilidad poderosos, como etiquetas, convertidores, funciones auxiliares y parámetros de plantillas personalizados. Esta es la sintaxis para llamar cada uno de ellos:

{{myConverter:name}}
{{myTag name}}
{{:~myHelper(name)}}
{{:~myParameter}}

Cada uno de estos tiene diferentes propósitos. Sin embargo, se pueden superponer un poco, según la situación. Antes de mostrar cómo elegir entre ellos, es importante comprender qué es lo que hace cada uno de ellos y cómo definirlos.

Las etiquetas personalizadas son ideales cuando necesitamos representar algo que tiene características de “tipo control” y puede ser independiente. Por ejemplo, las calificaciones con estrellas se pueden representar simplemente como un número, mediante datos, del siguiente modo:

{{:rating}}

Sin embargo, puede resultar mejor usar lógica en JavaScript para representar las calificaciones con estrellas mediante CSS y una serie de imágenes de estrellas vacías y llenas.

{{createStars averageRating max=5/}}

La lógica para crear las estrellas se puede (y debería) separar de la presentación. JsRender proporciona una forma de crear una etiqueta personalizada que encapsula esta funcionalidad. El código de la Figura 4 define una etiqueta personalizada llamada createStars y la registra en JsRender, para poder usarla en cualquier página que cargue este script. Para usar esta etiqueta personalizada hay que incluir el archivo JavaScript, jsrender.tag.js en el ejemplo, en la página.

Figura 4 Creación de una etiqueta personalizada

$.views.tags({
  createStars: function (rating) {
    var ratingArray = [], defaultMax = 5;
    var max = this.props.max || defaultMax;
    for (var i = 1; i <= max; i++) {
      ratingArray.push(i <= rating ? 
        "rating fullStar" : "rating emptyStar");
    }
    var htmlString = "";
    if (this.tmpl) {
      // Use the content or the template passed in with the template property.
      htmlString = this. renderContent(ratingArray);
    } else {
        // Use the compiled named template.
        htmlString = $.render.compiledRatingTmpl(ratingArray);
    }
    return htmlString;
  }

Las etiquetas personalizadas poseen propiedades declarativas, como por ejemplo la propiedad max=5 de {{createStars}} que se mostró anteriormente. En el código se accede a ellas a través de this.props. Por ejemplo, el siguiente código registra una etiqueta personalizada llamada sort que acepta una matriz (si la propiedad llamada reverse se establece en verdadero, {{sort array reverse=true/}}, la matriz se devuelve en orden inverso):

$.views.tags({
sort: function(array){
  var ret = "";
  if (this.props.reverse) {
    for (var i = array.length; i; i--) {
      ret += this.tmpl.render(array[i - 1]);
    }
  } else {
      ret += this.tmpl.render(array);
  }
  return ret;
}}

Una buena regla general es usar las etiquetas personalizadas cuando haya que representar algo un poco más complejo (como una etiqueta createStars o sort) y que se podría reutilizar. Las etiquetas personalizadas resultan menos prácticas en las situaciones de un solo uso.

Convertidores

Mientras que las etiquetas personalizadas son ideales para crear contenidos, los convertidores son una opción mejor para la tarea sencilla de convertir un valor fuente en un valor diferente. Los convertidores pueden cambiar valores fuente (como un valor booleano de verdadero o falso) en algo completamente diferente (como el color verde o rojo, respectivamente). Por ejemplo, el siguiente código usará el convertidor priceAlert para devolver una cadena que contiene una alerta de precio a partir del valor de salePrice:

    <div class="text highlightText">{{priceAlert:salePrice}}</div>

Los convertidores también son excelente para cambiar direcciones URL, como se aprecia aquí:

    <img src="{{ensureUrl:boxArt.smallUrl}}" class="rightAlign"/>

En el siguiente ejemplo, el convertidor ensureUrl debería convertir el valor de boxArt.smallUrl en una dirección URL calificada (ambos convertidores se usan en el archivo 12-converters.html y se registran en jsrender.helpers.js mediante la función $.views.converters de JsRender):

$.views.converters({
  ensureUrl: function (value) {
    return (value ? value : "/images/icon-nocover.png");
  },
  priceAlert: function (value) {0
    return (value < 10 ? "1 Day Special!" : "Sale Price");
  }
});

Los convertidores se diseñaron para la conversión no parametrizada de datos en un valor representado. Cuando la situación requiere de parámetros, entonces las funciones auxiliares o las etiquetas personalizadas son alternativas mejores que los convertidores. Tal como vimos, las etiquetas personalizadas permiten parámetros con nombres, por lo que la etiqueta createStars puede tener parámetros para definir el tamaño de las estrellas, sus colores, las clases CSS que se deben aplicar, etc. El punto importante aquí es que los convertidores son útiles para realizar conversiones sencillas, mientras que las etiquetas personalizadas sirven para representaciones más complejas listas para usar.

Funciones auxiliares y parámetros de plantillas

Tenemos varias formas para pasar funciones auxiliares o parámetros para su uso en la representación de las plantillas. Una consiste en registrarlas mediante $.views.helpers, de manera semejante al registro de las etiquetas y convertidores:

$.views.helpers({
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

Esto hará que estén disponibles para todas las plantillas en la aplicación. Otra forma es pasarlas como opciones en la llamada para la representación:

$.render.myTemplate( data, {
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

Este código hace que estén disponibles únicamente en el contexto de una llamada de representación de plantilla determinada. Sea como sea, las funciones auxiliares se pueden acceder desde dentro de la plantilla con el prefijo “~” en el parámetro o nombre de función (o ruta):

{{: ~extPrice(~todaysPrices.unitPrice, qty) }}

Las funciones auxiliares pueden hacer prácticamente de todo, incluso convertir datos, realizar cálculos, ejecutar lógica de aplicaciones, devolver matrices y objetos, e incluso devolver una plantilla.

Por ejemplo, podríamos crear una función auxiliar llamada getGuitars para buscar dentro de una matriz de productos y encontrar todas las guitarras. También podría aceptar un parámetro para el tipo de guitarra. El resultado se podría usar luego para representar un valor único o para iterar a través de la matriz resultante (ya que las funciones auxiliares pueden devolver lo que sea). El siguiente código puede obtener una matriz de todos los productos que sean guitarras acústicas e iterar sobre estos con un bloque {{for}}:

{{for ~getGuitars('acoustic')}} ... {{/for}}

Las funciones auxiliares también pueden llamar otras funciones auxiliares, como por ejemplo calcular un precio total con una matriz de los elementos de línea de un pedido y aplicar tarifas de descuento o tasas de impuestos:

{{:~totalPrice(~extendedPrice(lineItems, discount), taxRate}}

Las funciones auxiliares disponibles para varias plantillas se definen al pasar un literal de objeto que contenga las funciones auxiliares a la función $.views.helpers de JsRender. En el siguiente ejemplo, se define la función concat para concatenar varios argumentos:

$.views.helpers({
  concat:function concat() {
    return "".concat.apply( "", arguments );
  }
})

La función auxiliar concat se puede invocar con {{:~concat(nombre, edad, apellido)}}. Si suponemos que los valores de primero, edad y apellido son accesibles y que son John, 25 y Doe, respectivamente, entonces se representará el valor John25Doe.

Funciones auxiliares para situaciones únicas

Es posible que se encuentre en una situación en la que desea usar una función auxiliar para una plantilla específica, pero no tiene la intención de reutilizarla en otras plantillas. Por ejemplo, la plantilla de un carrito de compras puede requerir de un cálculo que es único para dicha plantilla. El cálculo se puede realizar con una función auxiliar, pero no hace falta que esta esté disponible para todas las plantillas. JsRender admite este escenario con el segundo enfoque mencionado anteriormente, pasar la función con las opciones en una llamada de representación:

$.render.shoppingCartTemplate( data, {
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

En este caso, se representa la plantilla del carrito de compras y las funciones auxiliares y los parámetros de la plantilla necesarios para realizar los cálculos se proporcionan directamente con la llamada de representación. La clave es que en este caso la función auxiliar solo existe durante la representación de esta plantilla puntual.

¿Cuál debemos usar?

JsRender ofrece varias opciones para crear plantillas poderosas con convertidores, etiquetas personalizadas y funciones auxiliares, pero es importante saber en qué situación se debe usar cada una. Una buena regla general es usar el árbol de decisión que aparece en la Figura 5, en la que se perfila cómo decidir cuál de estas características se debe usar.

Figura 5 Árbol de decisión para elegir la característica auxiliar correcta

if (youPlanToReuse) {
  if (simpleConversion && !parameters){
    // Register a converter.
  }
  else if (itFeelsLikeAControl && canBeSelfContained){
    // Register a custom tag.
  }
  else{
    // Register a helper function.
  }
}
else {
  // Pass in a helper function with options for a template.
}

Si la función se usará una sola vez, no hay necesidad de crear la sobrecarga para que sea accesible desde toda la aplicación. Esta es la situación ideal para una función auxiliar de “una sola vez” que se pasa solo cuando es necesaria.

Permitir código

Es posible que en alguna situación resulte más fácil escribir código personalizado dentro de una plantilla. JsRender permite insertar código, pero le recomiendo que solo lo haga cuando no le quede otra alternativa, ya que dificulta el mantenimiento al mezclar la presentación con el comportamiento. 

El código se puede incrustar dentro de una plantilla al encapsularlo dentro de un bloque con el prefijo asterisco {{* }} y al establecer allowCode en verdadero. Por ejemplo, la plantilla llamada myTmpl (que aparece en la Figura 6) contiene código para evaluar los lugares apropiados para representar un comando o la palabra “and” en varios idiomas. Podrá encontrar el ejemplo completo en el archivo 13-allowcode.html. La lógica no resulta tan complicada, pero el código puede ser difícil de leer en la plantilla.

JsRender no permitirá que se ejecute el código a menos que la propiedad allowCode se configure como verdadera (el valor predeterminado es falso). El siguiente código define la plantilla compilada llamada movieTmpl, le asigna el marcado de la etiqueta script que aparece en la Figura 6 y establece allowCode en verdadero en la plantilla:

$.templates("movieTmpl", {
  markup: "#myTmpl",
  allowCode: true
});
$("#movieRows").html(
  $.render.movieTmpl(my.vm.movies)
);

Una vez creada la plantilla, se representa. La función allowCode puede tener como resultado un código que cuesta leer y en algunos casos el trabajo se puede realizar con una función auxiliar. En este caso, el ejemplo de la Figura 6 usa la función allowCode de JsRender para agregar comas y la palabra “and” donde resulta necesario. Sin embargo, esto también se puede lograr con una función auxiliar:

$.views.helpers({
  languagesSeparator: function () {
    var view = this;
    var text = "";
    if (view.index === view.parent.data.length - 2) {
      text = " and";
    } else if (view.index < view.parent.data.length - 2) {
      text = ",";
    }
    return text;
  }
})

Figura 6 Permitir código en las plantillas

<script id="myTmpl" type="text/x-jsrender">
  <tr>
    <td>{{:name}}</td>
    <td>
      {{for languages}}
        {{:#data}}{{*
          if ( view.index === view.parent.data.length - 2 ) {
        }} and {{*
          } else if ( view.index < view.parent.data.length - 2 ) {
        }}, {{* } }}
      {{/for}}
    </td>
  </tr>
</script>

Para llamar esta función auxiliar languagesSeparator se coloca el prefijo “~” al nombre. Esto hace que el código de la plantilla que llama al auxiliar se pueda leer mejor, como se aprecia a continuación:

{{for languages}}
  {{:#data}}{{:~languagesSeparator()}}
{{/for}}

Al mover la lógica hacia una función auxiliar se elimina el comportamiento de la plantilla y se desplaza a JavaScript, lo que concuerda con los patrones de separación recomendados.

Rendimiento y flexibilidad

JsRender ofrece una serie de características que van mucho más allá de representar valores de propiedades, como por ejemplo el uso de expresiones complejas, la iteración y el cambio de contexto con el uso de la etiqueta {{for}} y rutas de visualización para navegar contextos. También proporciona los medios necesarios para extender sus características al agregar etiquetas personalizadas, convertidores y auxiliares, en caso de que sean necesarios. Estas características y el sistema basado únicamente en cadenas para crear plantillas permite un rendimiento excelente en JsRender y lo hace muy flexible.

John Papa fue un evangelizador de Microsoft en los equipos de Silverlight Windows 8, donde animó el programa de televisión popular “Silverlight TV”. También presentó conferencias magistrales y sesiones en BUILD, MIX, PDC, TechEd, Visual Studio Live! y DevConnections. Papa también es director regional de Microsoft, columnista de Visual Studio Magazine (Papa’s Perspective) y autor de vídeos de entrenamiento con Pluralsight. Puede seguirlo por Twitter en twitter.com/john_papa.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Boris Moore