Buy
Buy

Full-JavaScript Rendering & FOSJsRoutingBundle

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

Login Subscribe

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:

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

  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:

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

Leave a comment!

  • 2018-05-31 Shaun

    Thanks weaverryan , much appreciated :)

  • 2018-05-29 weaverryan

    Hey Shaun!

    There are 2 good options:

    1) Use FOSJsRoutingBundle
    2) Just hardcode the URL. I know this sounds "weird", but it's actually not a bad option. Why? Well, you should think of your API the same way you think of your PHP code. What I mean is, in PHP, if you, for example, rename a method, you would expect that this might break other parts of your app. And so, IF you want to make that change, you know that you need to find affected code. The same should be true for your API: IF you change something (like the URL), you are breaking compatibility with anyone that uses it (in this case, your JavaScript). That's no big deal: you can update your JavaScript of course. But, the point is: your API should not be something that changes randomly or often: you should treat it as something that typically does not change. So, I don't see a huge problem hardcoding URLs. Honestly, if you wrote a JavaScript app that talked to a third-party API (e.g. the GitHub API), you would totally hardcode the URLs to the GitHub API and it would never look weird.

    I hope this helps!

    Cheers!

  • 2018-05-27 Shaun

    If you weren't including the links value back from your replLog model, how would you add a route to the underscore.js template?

  • 2018-04-06 Victor Bocharsky

    Hey Daniel,

    Let's see... looks like this release already has Symfony 4 compatibility: https://github.com/FriendsO... . So I'd say yes, it should be compatible with Symfony 4 and should work, but if you find any issues - feel free to report them in the bundle's repository.

    Cheers!

  • 2018-04-06 Daniel Kronika

    Hi! Do you know if FOSJsRoutingBundle is already compatible with Symfony 4?
    Thanks a lot for your tutorials and courses - I love them and they helped me a lot!!!

  • 2018-03-09 Diego Aguiar

    Awesome!
    I believe if you update your Symfony's version as well, you won't get that error again

  • 2018-03-09 David Patterson

    Diego,

    Thanks for the information and the link.
    Turns out that in spite of all of the errors that it displayed, the bundle's functionality worked anyway. :-)

    Thanks very much for the responses.
    Dave
    --

  • 2018-03-08 Diego Aguiar

    This is a known issue, look: https://github.com/braincra...
    Did you try updating SensioDistributionBundle?

  • 2018-03-08 David Patterson

    Diego,

    PHP 7.1.15-1+ubuntu16.04.1+deb.sury.org+2 (cli) (built: Mar 6 2018 11:10:13) ( NTS )

    Thanks,
    D.
    --

  • 2018-03-08 Diego Aguiar

    Hey David Patterson

    Which version of PHP are you using?
    Try updating "sensio/distribution-bundle"


    composer update sensio/distribution-bundle

    Cheers!

  • 2018-03-08 David Patterson

    Installing the FOS jsrouting bundle failed.
    This is Symfony 3.1.6 as installed from your download for this course.

    The error output:
    [Symfony\Component\DependencyInjection\Exception\InvalidArgumentException]
    Unable to parse file "/Volumes/Data01/Projects/KNPUniversity/code-javascript/start/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/../Resources/config/web.xml".

    [InvalidArgumentException]
    The XML file "/Volumes/Data01/Projects/KNPUniversity/code-javascript/start/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/../Resources/config/web.xml" is not valid.

    [Symfony\Component\Debug\Exception\ContextErrorException]
    Notice: Undefined property: DOMDocument::$documentElement

    Script Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::clearCache handling the symfony-scripts event terminated with an exception

    There were also a lot of PHP warnings about modules having already been loaded. Probably safe to ignore.

    Perhaps I need to install a specific version since this course is more than a year old now?

    Thanks.

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