Full-JavaScript Rendering & FOSJsRoutingBundle
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen you try to render some things on the server, but then also want to update them dynamically in JavaScript, you're going to run into our new problem: template duplication. There are kind of two ways to fix it. First, if you use Twig like I do, there is a library called twig.js for JavaScript. In theory, you can write one Twig template and then use it on your server, and also in JavaScript. I've done this before and know of other companies that do it also.
My only warning is to keep these shared templates very simple: render simple variables - like categoryName
instead of product.category.name
- and try to avoid using many filters, because some won't work in JavaScript. But if you keep your templates simple, it works great.
The second, and more universal way is to stop rendering things on your server. As soon as I decide I need a JavaScript template, the only true way to remove duplication is to remove the duplicated server-side template and render everything via JavaScript.
Inside of our object, add a new function called loadRepLogs
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 31 | |
loadRepLogs: function() { | |
// ... lines 33 - 38 | |
}, | |
// ... lines 40 - 141 | |
}); | |
// ... lines 143 - 160 | |
})(window, jQuery, Routing); |
Call this from our constructor:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
window.RepLogApp = function ($wrapper) { | |
this.$wrapper = $wrapper; | |
this.helper = new Helper(this.$wrapper); | |
this.loadRepLogs(); | |
// ... lines 9 - 24 | |
}; | |
// ... lines 26 - 160 | |
})(window, jQuery, Routing); |
Because here's the goal: when our object is created, I want to make an AJAX call to and endpoint that returns all of my current RepLogs. We'll then use that to build all of the rows by using our template.
I already created the endpoint: /reps
:
// ... lines 1 - 13 | |
class RepLogController extends BaseController | |
{ | |
/** | |
* @Route("/reps", name="rep_log_list") | |
* @Method("GET") | |
*/ | |
public function getRepLogsAction() | |
{ | |
$repLogs = $this->getDoctrine()->getRepository('AppBundle:RepLog') | |
->findBy(array('user' => $this->getUser())) | |
; | |
$models = []; | |
foreach ($repLogs as $repLog) { | |
$models[] = $this->createRepLogApiModel($repLog); | |
} | |
return $this->createApiResponse([ | |
'items' => $models | |
]); | |
} | |
// ... lines 35 - 129 | |
} |
We'll look at exactly what this returns in a moment.
Getting the /reps URL
But first, the question is: how can we get this URL inside of JavaScript? I mean, we could hardcode it, but that should be your last option. Well, I can think of three ways:
We could add a
like on thedata-
attribute to something,$wrapper
element inindex.html.twig
.We could pass the URL into our
just like we're doing withRepLogApp
object via a second argument to the constructor,$wrapper
.If you're in Symfony, you could cheat and use a cool library called FOSJsRoutingBundle.
Using FOSJsRoutingBundle
Google for that, and click the link on the Symfony.com documentation. This allows you to expose some of your URLs in JavaScript. Copy the composer require line, open up a new tab, paste that and hit enter:
composer require "friendsofsymfony/jsrouting-bundle:^1.6"
While Jordi is wrapping our package with a bow, let's finish the install instructions. Copy the new bundle line, and add that to app/AppKernel.php
:
// ... lines 1 - 5 | |
class AppKernel extends Kernel | |
{ | |
public function registerBundles() | |
{ | |
$bundles = [ | |
// ... lines 11 - 21 | |
new FOS\JsRoutingBundle\FOSJsRoutingBundle(), | |
// ... lines 23 - 24 | |
]; | |
// ... lines 26 - 34 | |
} | |
// ... lines 36 - 55 | |
} |
We also need to import some routes: paste this into app/config/routing.yml
:
// ... lines 1 - 13 | |
fos_js_routing: | |
resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml" |
Finally, we need to add two script tags to our page. Open base.html.twig
and paste them at the bottom:
// ... lines 1 - 90 | |
{% block javascripts %} | |
// ... lines 92 - 94 | |
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script> | |
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script> | |
{% endblock %} | |
// ... lines 98 - 101 |
This bundle exposes a global variable called Routing
. And you can use that Routing
variable to generate links in the same way that we use the path
function in Twig templates: just pass it the route name and parameters.
Check the install process. Ding!
Tip
If you have a JavaScript error where Routing
is not defined, you may need to run:
php bin/console assets:install
Now, head to RepLogController
. In order to make this route available to that Routing
JavaScript variable, we need to add options={"expose" = true}
:
// ... lines 1 - 13 | |
class RepLogController extends BaseController | |
{ | |
/** | |
* @Route("/reps", name="rep_log_list", options={"expose" = true}) | |
// ... line 18 | |
*/ | |
public function getRepLogsAction() | |
// ... lines 21 - 129 | |
} |
Back in RepLogApp
, remember that this library gives us a global Routing
object. And of course, inside of our self-executing function, we do have access to global variables. But as a best practice, we prefer to pass ourselves any global variables that we end up using. So at the bottom, pass in the global Routing
object, and then add Routing
as an argument on top:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 160 | |
})(window, jQuery, Routing); |
Making the AJAX Call
Back down in loadRepLogs
, let's get to work: $.ajax()
, and set the url
to Routing.generate()
, passing that the name of our route: rep_log_list
. And on success
, just dump that data:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 31 | |
loadRepLogs: function() { | |
$.ajax({ | |
url: Routing.generate('rep_log_list'), | |
success: function(data) { | |
console.log(data); | |
} | |
}); | |
}, | |
// ... lines 40 - 141 | |
}); | |
// ... lines 143 - 160 | |
})(window, jQuery, Routing); |
Ok, go check it out! Refresh! You can see the GET
AJAX call made immediately. And adding a new row of course still works.
But look at the data sent back from the server: it has an items
key with 24 entries. Inside, each has the exact same keys that the server sends us after creating a new RepLog. This is huge: these are all the variables we need to pass into our template!
Rendering All the Rows in JavaScript
In other words, we're ready to go! Back in index.html.twig
, find the <tbody>
and empty it entirely: we do not need to render this stuff on the server anymore:
// ... lines 1 - 2 | |
{% block body %} | |
<div class="row"> | |
<div class="col-md-7 js-rep-log-table"> | |
// ... lines 6 - 12 | |
<table class="table table-striped"> | |
// ... lines 14 - 21 | |
<tbody> | |
</tbody> | |
// ... lines 24 - 31 | |
</table> | |
// ... lines 33 - 36 | |
</div> | |
// ... lines 38 - 44 | |
</div> | |
{% endblock %} | |
// ... lines 47 - 76 |
In fact, we can even delete our _repRow.html.twig
template entirely!
Let's keep celebrating: inside of LiftController
- which renders index.html.twig
- we don't need to pass in the repLogs
or totalWeight
variables to Twig: these will be filled in via JavaScript. Delete the totalWeight
variable from Twig:
// ... lines 1 - 10 | |
class LiftController extends BaseController | |
{ | |
// ... lines 13 - 15 | |
public function indexAction(Request $request) | |
{ | |
// ... lines 18 - 35 | |
return $this->render('lift/index.html.twig', array( | |
'form' => $form->createView(), | |
'leaderboard' => $this->getLeaders(), | |
)); | |
} | |
// ... lines 41 - 69 | |
} |
If you refresh the page now, we've got a totally empty table. Perfect. Back in loadRepLogs
, use $.each()
to loop over data.items
. Give the function key
and repLog
arguments:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 31 | |
loadRepLogs: function() { | |
// ... line 33 | |
$.ajax({ | |
url: Routing.generate('rep_log_list'), | |
success: function(data) { | |
$.each(data.items, function(key, repLog) { | |
// ... line 38 | |
}); | |
} | |
}); | |
}, | |
// ... lines 43 - 144 | |
}); | |
// ... lines 146 - 163 | |
})(window, jQuery, Routing); |
Finally, above the AJAX call, add var self = this
. And inside, say self._addRow(repLog)
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 31 | |
loadRepLogs: function() { | |
var self = this; | |
$.ajax({ | |
url: Routing.generate('rep_log_list'), | |
success: function(data) { | |
$.each(data.items, function(key, repLog) { | |
self._addRow(repLog); | |
}); | |
} | |
}); | |
}, | |
// ... lines 43 - 144 | |
}); | |
// ... lines 146 - 163 | |
})(window, jQuery, Routing); |
And that should do it! Refresh the page! Slight delay... boom! All the rows load dynamically: we can delete them and add more. Mission accomplished!
Hello I am experiencing a small problem that is throwing down all my effort to implement the js underscore at the very last step. I manage correctly to convert a very symple template to an html string. After that all my efforts to append the resulting html string "<tr>1</tr>" to the tbody element results in only the "<tr></tr>" being added inside the <tbody></tbody>. No matter I use .append($.parseHTML(html)) or just .html(html) the result is the same. Does anybody understand why my inner span tag and it´s content goes lost? (console.log(html) throws the full string with the number inside).