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.
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 data-
attribute to something,
$wrapper
element in index.html.twig
.We could pass the URL into our RepLogApp
object via a second argument to the constructor,
$wrapper
.If you're in Symfony, you could cheat and use a cool library called 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); |
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!
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!
ok stupid mistake of mine, I forgot to embed the <td></td> tags so the html parse was missing the content rightfully.
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!
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?
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
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 :)
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!
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.
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.
Hey David P.
Which version of PHP are you using?
Try updating "sensio/distribution-bundle"
composer update sensio/distribution-bundle
Cheers!
Diego,
PHP 7.1.15-1+ubuntu16.04.1+deb.sury.org+2 (cli) (built: Mar 6 2018 11:10:13) ( NTS )
Thanks,
D.
--
This is a known issue, look: https://github.com/braincra...
Did you try updating SensioDistributionBundle?
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
--
Awesome!
I believe if you update your Symfony's version as well, you won't get that error again
If you weren't including the links value back from your replLog model, how would you add a route to the underscore.js template?
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!
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!!!
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!
// 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
}
}
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).