How To Translate An Underscore Template To A Handlebars Template?
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 JavaScriptexpression
in the text. This is called an interpolation tag.<%- expression %>
will do the same, but HTML-escaped.<% code %>
lets you write arbitrary JavaScriptcode
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 thefor
. 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 findprint(expression)
. This is a shorthand meant to avoid having to break out of the code tag, insert an interpolation tag withexpression
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 keyname
in the template.{{{name}}}
does the same, but without HTML-escaping.{{#if condition}}...{{/if}}
will insert the...
part only ifcondition
is met.{{#each name}}...{{/each}}
will repeat...
for each element or property ofname
.name
becomes the context of the...
; that is, if you write{{otherName}}
within...
, Handlebars will try to findotherName
as a property of the object identified byname
.{{#name}}...{{/name}}
is a notation that Handlebars inherits from Mustache. It behaves similar to{{#each name}}
whenname
is an array and similar to{{#if name}}
otherwise (also changing the context toname
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:
- Replace any occurrences of
print(_.escape(expression))
that appear inside any<%...%>
tag by%><%- expression %><%
. - Replace any other occurrences of
print(expression)
inside<%...%>
by%><%= expression %><%
. - Replace all occurrences of
<%- expression %>
(including ones that were already there before step 1) by{{expression}}
. - Replace all occurrences of
<%= expression %>
(including ones that were already there before step 2) by{{{expression}}}
. - If you find aliases of the form
var name = otherName.propertyName
anywhere (except within the parentheses of afor (...)
statement), substituteotherName.propertyName
forname
everwhere this variable is in scope and drop the variable. Don't do this with loop variables insidefor (...)
; those are replaced in step 7. - 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 moreelse if
. Note that the final<% } %>
stays in place; you're inserting one additional{{/if}}
in front of it for every intermediateelse if
. - Replace the outermost
<% if (condition1) { %>...<% } %>
by{{#if condition1}}...{{/if}}
. This time, the final<% } %>
disappears.
- Replace the final
- 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 ofkeyName
,collectionName
orobjectName
and replace them by by@key
,..
and..
respectively (that is not a typo;collectionName
andobjectName
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 casecollectionName
and evenkeyName
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 ofindexName
,collectionName
orarrayName
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 ofkeyName
,objectName[keyName]
orobjectName
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 ofindexName
,arrayName[indexName]
orarrayName
and replace them by by@index
,this
and..
respectively.
- Replace functional loops over objects of the form
- 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
orarrayName
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 intoname.[index].[index2]
(note the periods).
- If you have created expressions of the form
- Check whether all
expression
s andcondition
s 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 examplekeyName
orkeyName.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 justpropertyName
, becausethis
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.
- Note that
- 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?"