If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHere'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"> | |
... lines 69 - 79 | |
</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"> | |
... lines 72 - 79 | |
</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"> | |
</script> | |
{% endblock %} |
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"> | |
... lines 69 - 71 | |
... line 77 | |
</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.
Is there a reason why underscorejs was just used as a global library, rather than passing it into the function like was done with `window`, `jQuery` and, eventually, `Routing` and `swal`?
Hey tomchkkk!
No reason at all :). Actually, good eye - that was a mistake! With the self-executing blocks, you're trying to be "responsible" by passing what you need into the function. But, unfortunately, all of these variables - jQuery, Routing, _ - *are* truly global variables... which you're basically trying to convert to local variables while inside the function. But, you can see how easy it is to forget a detail, and suddenly start using the global variable.
In our Webpack tutorial (https://knpuniversity.com/s... or Webpack Encore tutorial (https://knpuniversity.com/s... - Encore is just a "shortcut" around Webpack) we talk about "module loading", which is an improvement on this method, and a situation where I would *not* have been able to make this mistake.
Cheers!
Hey guys,
So, if your front end communicates with the back end via an API then it seems to me that all templates for an app would need to use Underscore rather than Twig, is that right?
Hey Shaun T.
You are correct, Twig is used for backend rendering, but you don't have to use "Underscore" obligatory, you may want to use ReactJS or any other template engine for frontend (psst, utilize ReactJS)
Cheers!
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?
Hey Thao L.
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!
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...
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!
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!
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!
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
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!
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!
// 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
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"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
}
}
I have this error when I send the request
`
VM40780:8 Uncaught ReferenceError: trans is not defined