JavaScript Templating

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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:

... 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 %>:

... 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!

... 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 %>:

... 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():

... 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):

... 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:

... 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)):

... 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():

... 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:

... 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!

This tutorial uses an older version of Symfony... but since it's a JavaScript tutorial, the concepts are still 💯 valid!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.1.*", // v3.1.10
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.1
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "symfony/swiftmailer-bundle": "^2.3", // v2.4.0
        "symfony/monolog-bundle": "^2.8", // 2.12.0
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0@dev", // dev-master
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.1
        "symfony/phpunit-bridge": "^3.0" // v3.1.6
    }
}