Skip to content Skip to sidebar Skip to footer

How To Translate An Underscore Template To A Handlebars Template?

I'm upgrading a Shopify store that's using an old theme. In the (old) Cart page is code for a 'Shipping Estimator' which (because it works well) they want to re-use in the new them

Solution 1:

Normally, Underscore and Handlebars are not really alternatives to each other. Underscore is a toolkit with general functional utilities, which helps you write shorter, more maintainable code in functional style. Handlebars, on the other hand, is a library entirely dedicated to template rendering, which helps you write cleaner, more maintainable templates.

When using Underscore, you may find its functions being called everywhere throughout your JavaScript code, while Handlebars is only called in places where you'll be rendering a template. For this reason, these libraries normally don't conflict at all; it is perfectly possible to write an application that depends on both (in fact I've been doing this for a while in most of my applications). Just link both libraries into your page,

<scriptsrc="https://cdn.jsdelivr.net/npm/underscore@1.12.0/underscore-min.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/handlebars@4.7.7/dist/handlebars.js"></script>

or import both as modules,

import _ from'underscore';
import * asHandlebarsfrom'handlebars';

or do the same with an older module format such as AMD or CommonJS.

However, Underscore does have a template function, which is a very minimal templating implementation that can be used as an alternative to a library like Handlebars. This appears to be the case in the old version of your application, and it is causing conflicts because Underscore's minimal templating language is different from Handlebars's templating language. The compilation and rendering steps are also a bit different between these libraries.

Comparing the template languages

Both template languages let you insert special tags in a piece of text in order to make parts of that text parametric, conditional or repetitive. The tags that they support are however different.

Underscore:

  • <%= expression %> will insert the string value of the JavaScript expression in the text. This is called an interpolation tag.
  • <%- expression %> will do the same, but HTML-escaped.
  • <% code %> lets you write arbitrary JavaScript code that will make parts of the template conditional or repetitive. Often, you'll find that one such tag goes something like <% for (...) { %> and then a bit further down the template, there is a matching <% } %>. The part of the template between those two code tags is then a loop that will repeat by the logic of the for. Similarly, you may find <% if (...) { %>...<% } %> to make the ... part of the template conditional. (Honestly, this is quite ugly, but it helps the implementation to be minimal. Underscore's template module is only one page.)
  • Inside the code part of <% code %> you may occasionally find print(expression). This is a shorthand meant to avoid having to break out of the code tag, insert an interpolation tag with expression and then immediately resume with a new code tag. In other words, <% code1 print(expression) code2 %> is a shorthand for <% code1 %><%= expression %><% code2 %>.

Handlebars:

  • {{name}} inserts the HTML-escaped string value of the property with key name in the template.
  • {{{name}}} does the same, but without HTML-escaping.
  • {{#if condition}}...{{/if}} will insert the ... part only if condition is met.
  • {{#each name}}...{{/each}} will repeat ... for each element or property of name. name becomes the context of the ...; that is, if you write {{otherName}} within ..., Handlebars will try to find otherName as a property of the object identified by name.
  • {{#name}}...{{/name}} is a notation that Handlebars inherits from Mustache. It behaves similar to {{#each name}} when name is an array and similar to {{#if name}} otherwise (also changing the context to name if it is an object). The idea behind this (in Mustache) is to make the template even more declarative, or "logic-free" as the authors call it.
  • There are more tags that I won't go into now.

Translating Underscore templates to Handlebars

Since Underscore allows the insertion of arbitrary JavaScript code in a template, it is not always possible to translate an Underscore template to an equivalent Handlebars template. Fortunately, however, templates don't really need the full expressive power of JavaScript, so Underscore templates are likely to be written in a way that can be ported to a more restrictive template language ("lucky by accident"). When it is possible, the following strategy will be sufficient most of the time:

  1. Replace any occurrences of print(_.escape(expression)) that appear inside any <%...%> tag by %><%- expression %><%.
  2. Replace any other occurrences of print(expression) inside <%...%> by %><%= expression %><%.
  3. Replace all occurrences of <%- expression %> (including ones that were already there before step 1) by {{expression}}.
  4. Replace all occurrences of <%= expression %> (including ones that were already there before step 2) by {{{expression}}}.
  5. If you find aliases of the form var name = otherName.propertyName anywhere (except within the parentheses of a for (...) statement), substitute otherName.propertyName for name everwhere this variable is in scope and drop the variable. Don't do this with loop variables inside for (...); those are replaced in step 7.
  6. If you find any pattern of the form <% if (condition1) { %>...<% } else if (condition2) { %>...<% } else ... if (conditionN) { %>...<% } else { %>...<% } %>, start with the last, innermost block and work your way outwards from there, as follows:
    • Replace the final <% } else { %> by {{else}} (Handlebars recognizes this as a special notiation).
    • Replace the final intermediate <% } else if (conditionN) { %>...<% } %> by {{else}}{{#if conditionN }}...{{/if}}<% } %>. Repeat this step until there is no more else if. Note that the final <% } %>stays in place; you're inserting one additional {{/if}} in front of it for every intermediate else if.
    • Replace the outermost <% if (condition1) { %>...<% } %> by {{#if condition1}}...{{/if}}. This time, the final <% } %> disappears.
  7. Replace loops, again starting with the innermost expressions and working your way outwards from there:
    • Replace functional loops over objects of the form <% _.each(objectName, function(valueName, keyName, collectionName) { %>...<% }) %> by the notation {{#each objectName}}...{{/each}}. Check nested {{}}/{{{}}}/{{#if}}/{{#each}} tags within ... for any appearances of keyName, collectionName or objectName and replace them by by @key, .. and .. respectively (that is not a typo; collectionName and objectName should both be replaced by .. because they refer to the same object). Note that the function passed to _.each in the Underscore version may take fewer arguments, in which case collectionName and even keyName may be absent; the replacement works the same regardless.
    • Replace functional loops over arrays of the form <% _.each(arrayName, function(valueName, indexName, collectionName) { %>...<% }) %> by the notation {{#each arrayName}}...{{/each}}. Check nested {{}}/{{{}}}/{{#if}}/{{#each}} tags within ... for any appearances of indexName, collectionName or arrayName and replace them by by @index, .. and .. respectively. Again, the function passed to _.each in the Underscore version may take fewer arguments; the replacement works the same regardless.
    • Replace procedural loops over objects of the form <% for (var keyName in objectName) { %>...<% } %> by the notation {{#each objectName}}...{{/each}}. Check nested {{}}/{{{}}}/{{#if}}/{{#each}} tags within ... for any appearances of keyName, objectName[keyName] or objectName and replace them by by @key, this and .. respectively.
    • Replace procedural loops over arrays of the form <% for (var indexName = 0; indexName < arrayName.length; ++indexName) { %>...<% } %> by the notation {{#each arrayName}}...{{/each}}. Check nested {{}}/{{{}}}/{{#if}}/{{#each}} tags within ... for any appearances of indexName, arrayName[indexName] or arrayName and replace them by by @index, this and .. respectively.
  8. Fix expression syntax:
    • If you have created expressions of the form ...propertyName in the previous step, where the first two periods .. were originally a name (objectName or arrayName as described under step 7), replace this by ../propertyName. You may have longer paths of this form, for example ../../propertyName.
    • Subexpressions of the form name[index1][index2] should be turned into name.[index].[index2] (note the periods).
  9. Check whether all expressions and conditions that you translated can be evaluated as-is by Handlebars. As a rule of thumb, Handlebars can only directly evaluate (nested) property names of the current context (for example keyName or keyName.subProperty) and some special notations that it recognizes such as @key, @index, @root, this and ... Use helpers to evaluate expressions that are more than just the name of some object and anchor names as necessary to @root or ..:
    • Note that this.propertyName is equivalent to just propertyName, because this refers to the current context.
    • When passing an object like {a: foo, b: {c: baz}} to the template, the properties of this outermost object can always be referenced with @root.a, @root.b.c, etcetera. Note that this object may have been given a name of its own inside the original Underscore template; in that case, this name itself can be replaced by @root.
    • .. is for referencing the parent context inside loops, as we have seen in steps 7-8. Occasionally, a loop in the original Underscore template may reference a property of a parent context directly by closing over it; in such cases, you can help Handlebars find the right property by prefixing the name of this property with ../ as necessary.
  10. You may have leftover empty <% %> tags after the previous transformations; these can be safely removed.

If, after the above steps, you still have <% code %> notation in your template, or expressions that Handlebars cannot evaluate, you may need to use other facilities from the Handlebars language or create special workarounds. If you are very unlucky, the template cannot be translated at all, but most of the time there will be a way.

Demonstration: your template

Repeating the Underscore template from your question here:

<pid="shipping-rates-feedback" <% if (success) { %> class="success" <% } else { %> class="error" <% } %>>
<% if (success) { %>
  <% if (rates.length > 1) { %>
  There are <%- rates.length %> shipping rates available for <%- address %>, starting at <%= rates[0].price %>.
  <% } else if (rates.length == 1) { %>
  There is one shipping rate available for <%- address %>.
  <% } else { %>
  We do not ship to this destination.
  <% } %>
<% } else { %>
  <%- errorFeedback %>
<% } %>
</p><ulid="shipping-rates">
  <% for (var i=0; i<rates.length; i++) { %>
  <li><%- rates[i].name %> at <%= rates[i].price %></li>
  <% } %>
</ul>

Following the above algorithm and using the expression rates.[1] instead of rates.length > 1 (because Handlebars cannot evaluate comparisons out of the box), we successfully obtain the following Handlebars template:

<pid="shipping-rates-feedback" {{#ifsuccess}} class="success" {{else}} class="error" {{/if}}>
{{#if success}}
  {{#if rates.[1]}}
  There are {{rates.length}} shipping rates available for {{address}}, starting at {{{rates.[0].price}}}.
  {{else}}{{#if rates}}
  There is one shipping rate available for {{address}}.
  {{else}}
  We do not ship to this destination.
  {{/if}}{{/if}}
{{else}}
  {{errorFeedback}}
{{/if}}
</p><ulid="shipping-rates">
  {{#each rates}}
  <li>{{name}} at {{{price}}}</li>
  {{/each}}
</ul>

You might find other templates that need to be translated as well. You can follow the same approach for those other templates.

Final note: templates embedded in HTML

Your theme includes the template in the page with the following notation.

<scriptid="shipping-calculator-response-template"type="text/template">// the template</script>

It is important to realize that, although this is a <script> tag, the browser does not actually interpret the contents as a script. Instead, because the tag has a type that the browser doesn't know, the browser just leaves the tag as-is in the DOM and moves on to interpret the next element. This is a common trick to embed arbitrary text data in an HTML page, so that it can be picked up by a script later without the user having to see it. In this particular case, there will be a piece of your JavaScript somewhere that will do something like

var templateText = document.querySelector('#shipping-calculator-response-template').textContent;

and then pass templateText to Handlebars in order to process it. This is also the reason why replacing Handlebars back by Underscore didn't solve your problem; that script will still try to pass the template to Handlebars. Concluding, in your particular case, there is probably no need to put back the Underscore reference.

Post a Comment for "How To Translate An Underscore Template To A Handlebars Template?"