Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

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

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:^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!

Leave a comment!

19
Login or Register to join the conversation
Juan-Etxenike Avatar
Juan-Etxenike Avatar Juan-Etxenike | posted 1 year ago

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

Reply
Juan-Etxenike Avatar

ok stupid mistake of mine, I forgot to embed the <td></td> tags so the html parse was missing the content rightfully.

Reply

Hey Juan,

I'm happy to hear you were able to find the problem, well done! Yeah, it happens, sometimes things that are easy to miss then hard to fix ;) And thanks for sharing your solution with others!

Cheers!

Reply
Götz V. Avatar
Götz V. Avatar Götz V. | posted 2 years ago

I always have problems with courses in older Symfony versions. So what's the best way to upgrade for example in this case from 3.1 to the latest version.
In this case I try to install the bundle with:
composer require friendsofsymfony/jsrouting-bundle
and get:

Using version ^2.7 for friendsofsymfony/jsrouting-bundle
./composer.json has been updated
Running composer update friendsofsymfony/jsrouting-bundle
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires friendsofsymfony/jsrouting-bundle ^2.7 -> satisfiable by friendsofsymfony/jsrouting-bundle[2.7.0].
- friendsofsymfony/jsrouting-bundle 2.7.0 requires symfony/framework-bundle ~3.3|^4.0|^5.0 -> found symfony/framework-bundle[v3.3.0, ..., v3.4.47, v4.0.0, ..., v4.4.25, v5.0.0, ..., v5.3.0] but these were not loaded, likely because it conflicts with another require.
Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.
Any hint?

Reply

Hey Götz V.

Sorry for late reply, yeah the latest bundle is not compatible with this course, as a solution install it with following command


composer require "friendsofsymfony/jsrouting-bundle:^1.6.0"

Cheers

Reply
Default user avatar
Default user avatar Imad Zairig | posted 5 years ago

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

Reply

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!

Reply
David P. Avatar
David P. Avatar David P. | posted 5 years ago

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.

Reply
CDesign Avatar
CDesign Avatar CDesign | David P. | posted 2 years ago | edited

If you have trouble getting jsrouting-bundle to install with 'composer require', try this:

1) in composer.json change the require line for jsrouting-bundle to: "friendsofsymfony/jsrouting-bundle": "~1.6.0"

- that's the version Ryan installed

2) and then run:

 `composer update friendsofsymfony/jsrouting-bundle`

If you get composer out-of-memory errors, execute the command like this:

 `php -d memory_limit=-1 /usr/local/bin/composer update friendsofsymfony/jsrouting-bundle`

replacing <i>/usr/local/bin/composer</i> with the path to your composer executable.

3) and then run:

php bin/console assets:install web --symlink

which the video does suggest.

And one last thing.. the link on the current Symfony/FOSJsRoutingBundle/Usage page is to the '../router.js.min' version. Change that to '../router.js' or simply copy the link from the course code.

1 Reply

Hey David P.

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


composer update sensio/distribution-bundle

Cheers!

Reply
David P. Avatar

Diego,

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

Thanks,
D.
--

Reply

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

Reply
David P. Avatar

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

Reply

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

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | posted 5 years ago

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

Reply

Hey Shaun T.!

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!

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | weaverryan | posted 5 years ago | edited

Thanks weaverryan , much appreciated :)

Reply
Default user avatar
Default user avatar Daniel Kronika | posted 5 years ago

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

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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
        "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
    }
}
userVoice