Full-JavaScript Rendering & FOSJsRoutingBundle

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

162 lines web/assets/js/RepLogApp.js
... 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:

162 lines web/assets/js/RepLogApp.js
... 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:

131 lines src/AppBundle/Controller/RepLogController.php
... 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:

  1. We could add a data- attribute to something, like on the $wrapper element in index.html.twig.

  2. We could pass the URL into our RepLogApp object via a second argument to the constructor, just like we're doing with $wrapper.

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

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:

57 lines 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:

16 lines 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:

101 lines app/Resources/views/base.html.twig
... 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}:

131 lines src/AppBundle/Controller/RepLogController.php
... 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:

162 lines web/assets/js/RepLogApp.js
... 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:

Array

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:

76 lines app/Resources/views/lift/index.html.twig
... 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:

71 lines src/AppBundle/Controller/LiftController.php
... 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:

165 lines web/assets/js/RepLogApp.js
... 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):

165 lines web/assets/js/RepLogApp.js
... 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!

Leave a comment!

  • 2017-03-22 weaverryan

    Thanks Imad Zairig! And you're right! Depending on how fast you install things, you may need to run this command (if you add the bundle to AppKernel before composer finishes, then composer will do this for you, but if not, it's necessary). We'll add a note to the script+video!

    Cheers!

  • 2017-03-19 Imad Zairig

    Hi ,
    I want to thank your for this great serie (y) ,
    for none Symfony users it will be nice to add the command php bin/console assets:install after the installation of FOSRoutingJs,

    thank you again :)