JavaScript Templating

Here's the goal: use a JavaScript template to render a new RepLog <tr> after we successfully submit the form. The first step is to, well, create the template - a big string with a mix of HTML and dynamic code. If you look at the Underscore.js docs, you'll see how their templates are supposed to look.

Now, we don't want to actually put our templates right inside JavaScript like they show, that would get messy fast. Instead, one great method is to add a new script tag with a special type="text/template" attribute. Give this an id, like js-rep-log-row-template, so we can find it later:

83 lines app/Resources/views/lift/index.html.twig
... lines 1 - 54
{% block javascripts %}
... lines 56 - 66
<script type="text/template" id="js-rep-log-row-template">
... lines 68 - 80
</script>
{% endblock %}

Tip

The text/template part doesn't do anything special at all: it's just a standard to indicate that what's inside is not actually JavaScript, but something else.

This is one of the few places where I use ids in my code. Inside, we basically want to duplicate the _repRow.html.twig template, but update it to be written for Underscore.js.

So temporarily, we are totally going to have duplication between our Twig, server-side template and our Underscore.js, client-side template. Copy all the <tr> code, then paste it into the new script tag.

Now, update things to use the Underscore.js templating format. So, <%= totalWeightLifted %>:

83 lines app/Resources/views/lift/index.html.twig
... lines 1 - 54
{% block javascripts %}
... lines 56 - 66
<script type="text/template" id="js-rep-log-row-template">
<tr data-weight="<%= totalWeightLifted %>">
... lines 69 - 79
</tr>
</script>
{% endblock %}

This is the print syntax, and I'm using a totalWeightLifted variable because eventually we're going to pass these keys to the template as variables: totalWeightLifted, reps, id, itemLabel and links.

Do the same thing to print out itemLabel. Keep going: the next line will be reps. And then use totalWeightLifted again... but make sure you use the right syntax!

83 lines app/Resources/views/lift/index.html.twig
... lines 1 - 54
{% block javascripts %}
... lines 56 - 66
<script type="text/template" id="js-rep-log-row-template">
<tr data-weight="<%= totalWeightLifted %>">
<td><%= itemLabel %></td>
<td><%= reps %></td>
<td><%= totalWeightLifted %></td>
... lines 72 - 79
</tr>
</script>
{% endblock %}

But what about this data-url? We can't use the Twig path function anymore. But we can use this links._self key! That's supposed to be the link to where we can GET info about this RepLog, but because our API is well-built, it's also the URL to use for a DELETE request.

Great! Print out <%= links._self %>:

83 lines app/Resources/views/lift/index.html.twig
... lines 1 - 54
{% block javascripts %}
... lines 56 - 66
<script type="text/template" id="js-rep-log-row-template">
<tr data-weight="<%= totalWeightLifted %>">
<td><%= itemLabel %></td>
<td><%= reps %></td>
<td><%= totalWeightLifted %></td>
<td>
<a href="#"
class="js-delete-rep-log"
data-url="<%= links._self %>"
>
<span class="fa fa-trash"></span>
</a>
</td>
</tr>
</script>
{% endblock %}

Rendering the Template

Gosh, that's a nice template. Let's go use it! Find our _addRow() function. First, find the template text: $('#js-rep-log-row-template').html():

151 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 121
_addRow: function(repLog) {
var tplText = $('#js-rep-log-row-template').html();
... lines 124 - 129
}
});
... lines 132 - 149
})(window, jQuery);

Done! Our script tag trick is an easy way to store a template, but we could have also loaded it via AJAX. Winning!

Next, create a template object: var tpl = _.template(tplText):

151 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 121
_addRow: function(repLog) {
var tplText = $('#js-rep-log-row-template').html();
var tpl = _.template(tplText);
... lines 125 - 129
}
});
... lines 132 - 149
})(window, jQuery);

That doesn't render the template, it just prepares it. Oh, and like before, my editor doesn't know what _ is... so I'll switch back to base.html.twig, press option+enter or alt+enter, and download that library. Much happier!

To finally render the template, add var html = tpl(repLog), where repLog is an array of all of the variables that should be available in the template:

151 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 121
_addRow: function(repLog) {
var tplText = $('#js-rep-log-row-template').html();
var tpl = _.template(tplText);
var html = tpl(repLog);
... lines 127 - 129
}
});
... lines 132 - 149
})(window, jQuery);

Finally, celebrate by adding the new markup to the table: this.$wrapper.find('tbody') and then .append($.parseHTML(html)):

151 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 121
_addRow: function(repLog) {
var tplText = $('#js-rep-log-row-template').html();
var tpl = _.template(tplText);
var html = tpl(repLog);
this.$wrapper.find('tbody').append($.parseHTML(html));
... lines 128 - 129
}
});
... lines 132 - 149
})(window, jQuery);

The $.parseHTML() function turns raw HTML into a jQuery object.

And since we have a new row, we also need to update the total weight. Easy! this.updateTotalWeightLifted():

151 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 121
_addRow: function(repLog) {
var tplText = $('#js-rep-log-row-template').html();
var tpl = _.template(tplText);
var html = tpl(repLog);
this.$wrapper.find('tbody').append($.parseHTML(html));
this.updateTotalWeightLifted();
}
});
... lines 132 - 149
})(window, jQuery);

Deep breath. Let's give this a shot. Refresh the page. I think we should lift our coffee cup ten times to stay in shape. Bah, error! Oh, that was Ryan being lazy: our endpoint returns a links key, not link. Let's fix that:

83 lines app/Resources/views/lift/index.html.twig
... lines 1 - 54
{% block javascripts %}
... lines 56 - 66
<script type="text/template" id="js-rep-log-row-template">
<tr data-weight="<%= totalWeightLifted %>">
... lines 69 - 71
<td>
<a href="#"
class="js-delete-rep-log"
data-url="<%= links._self %>"
>
... line 77
</a>
</td>
</tr>
</script>
{% endblock %}

Ok, refresh and try it gain! This time, let's lift our coffee cup 20 times! It's alive!!!

If you watch closely, it's even updating the total weight at the bottom.

I love it! Except for the massive duplication: it's a real bummer to have the row template in two places. Let me show you one way to fix this.

Leave a comment!

  • 2017-05-18 Daniel

    Thanks Ryan! I missed delete the Twig related variables on the template.

    Cheers

  • 2017-05-14 weaverryan

    Hey Daniel!

    I believe this is basically a "Variable not found" error when it renders the template. In other words, the template needs a repLog variable, but there isn't one available. If you look at the last code block on the page, it shows the finished template (the code inside of the js-rep-log-row-template script tag. You shouldn't actually have a repLog variable in your template. Does your template have this variable? If it *does* have the variable, kill it! Make your's look like mine :). If not, post some more code - I want to see exactly what's going on.

    Cheers!

  • 2017-05-14 Daniel

    Hi, I am getting a weird error over here:

    ```javascript
    VM1229:6 Uncaught ReferenceError: repLog is not defined
    at eval (eval at m.template (underscore.js:1454), <anonymous>:6:9)
    at c (underscore.js:1461)
    at window.RepLogApp._addRow (RepLogApp.js:127)
    at Object.success (RepLogApp.js:82)
    at i (jquery-3.1.1.min.js:2)
    at Object.fireWith [as resolveWith] (jquery-3.1.1.min.js:2)
    at A (jquery-3.1.1.min.js:4)
    at XMLHttpRequest.<anonymous> (jquery-3.1.1.min.js:4)
    (anonymous) @ VM1229:6
    c @ underscore.js:1461
    _addRow @ RepLogApp.js:127
    success @ RepLogApp.js:82
    i @ jquery-3.1.1.min.js:2
    fireWith @ jquery-3.1.1.min.js:2
    A @ jquery-3.1.1.min.js:4
    (anonymous) @ jquery-3.1.1.min.js:4
    ```

    The row doesn't get updated...

  • 2017-05-10 Victor Bocharsky

    Hey Julien,

    If you're in Twig template, you can get the current user id with {{ app.user.id }} and put it on any data property. I think it makes sense to set it on body tag, but it's up to you, for example:


    <body data-user-id="{{ app.user.id }}">
    </body>

    So if there's a logged in user or no - you'll know it in JS by parsing "user-id" data attribute with jQuery like "$('body').data('user-id')" or manually.

    Does it makes sense for you?

    Cheers!

  • 2017-05-09 julien moulis

    Hi everyone, is it possible to get the loggedin user in an underscore template?
    Thanks

  • 2017-03-30 Diego Aguiar

    Hey Thao Truong
    By using *$.parseHtml()* method you are ensuring that all the HTML injected it's been rendered correctly by the browser
    You can find more information about it here:
    https://api.jquery.com/jque...

    Have a nice day!

  • 2017-03-30 Thao Truong

    Hello, about this: this.$wrapper.find('tbody').append($.parseHTML(html));
    When I use
    this.$wrapper.find('tbody').append(html); it works exactly the same way, so why do we need to parse html here?

  • 2017-02-14 Victor Bocharsky

    Hey Max,

    If you have to pass 2 or more objects - then simply pass object of objects:


    var html = tpl({
    repLog: repLog,
    object2: object2
    });

    Cheers!

  • 2017-02-13 Max

    Hey Ryan!

    Cool! I never thought about using an associative array as twig parameter.

    But if I wanted to render two different JS-Objects I would need to add the object.(.xy...) prefix, right?

    Best

  • 2017-02-13 weaverryan

    Hi Max!

    Ah, it's just due to a *subtle* difference in how we're passing the variables to the Twig template versus the Underscore template - it's not due to any difference in how they work. Let me know show you :)

    // in Twig we pass an array of variables to the template. One variable is called repLog
    return $this->render('lift/index.html.twig', array(
    'form' => '...',
    // ... the variable will be called repLog
    'repLogs' => $repLogs,
    ));

    But in Underscore, we're pass the repLog itself as the array of arguments, so its keys become the variables.


    var html = tpl(repLog);
    // this would be equivalent to this:
    var html = tpl({
    // itemLabel and totalWeightLifted are the variables here
    itemLabel: repLog.itemLabel,
    totalWeightLifted: repLog.totalWeightLifted
    });

    Does that make sense? We could almost do the same thing in Twig:

    // IF repLog were an associative array, this would work in Twig, and would mean that
    // we would have variables like itemLabel and totalWeightLifted
    return $this->render('lift/index.html.twig', $repLog);

    Cheers!

  • 2017-02-13 Max

    Hey! I never worked with underscore-js. Why don't we have to write repLog.itemLabel, repLog.reps, ... in our template (as in twig files?)? Thx!