Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

JavaScript, AJAX & the Profiler

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Here's our next goal: write some JavaScript so that that when we click the up or down vote icons, it will make an AJAX request to our JSON endpoint. This "fakes" saving the vote to the database and returns the new vote count, which we will use to update the vote number on the page.

Adding js- Classes to the Template

The template for this page is: templates/question/show.html.twig. For each answer, we have these vote-up and vote-down links. I'm going to add a few classes to this section to help our JavaScript. On the vote-arrows element, add a js-vote-arrows class: we'll use that in JavaScript to find this element. Then, on the vote-up link, add a data attribute called data-direction="up". Do the same for the down link: data-direction="down". This will help us know which link was clicked. Finally, surround the vote number - the 6 - with a span that has another class: js-vote-total. We'll use that to find the element so we can update that number.

... lines 1 - 4
{% block body %}
<div class="container">
... lines 7 - 36
<ul class="list-unstyled">
{% for answer in answers %}
<li class="mb-4">
<div class="d-flex justify-content-center">
... lines 41 - 47
<div class="vote-arrows flex-fill pt-2 js-vote-arrows" style="min-width: 90px;">
<a class="vote-up" href="#" data-direction="up"><i class="far fa-arrow-alt-circle-up"></i></a>
<a class="vote-down" href="#" data-direction="down"><i class="far fa-arrow-alt-circle-down"></i></a>
<span>+ <span class="js-vote-total">6</span></span>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}

Adding JavaScript inside the javascripts Block.

To keep things simple, the JavaScript code we are going to write will use jQuery. In fact, if your site uses jQuery, you probably will want to include jQuery on every page... which means that we want to add a script tag to base.html.twig. At the bottom, notice that we have a block called javascripts. Inside this block, I'm going to paste a <script> tag to bring in jQuery from a CDN. You can copy this from the code block on this page, or go to jQuery to get it.

Tip

In new Symfony projects, the javascripts block is at the top of this file - inside the <head> tag. You can keep the javascripts block up in <head> or move it down here. If you keep it up inside head, be sure to add a defer attribute to every script tag: this will cause your JavaScript to be executed after the page loads.

... line 1
<html>
... lines 3 - 12
<body>
... lines 14 - 25
{% block javascripts %}
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>

If you're wondering why we put this inside of the javascripts block... other than it "seems" like a logical place, I'll show you why in a minute. Because technically, if we put this after the javascripts block or before, it would make no difference right now. But putting it inside will be useful soon.

For our custom JavaScript, inside the public/ directory, create a new directory called js/. And then a new file: question_show.js.

Here's the idea: usually you will have some custom JavaScript that you want to include on every page. We don't have any right now, but if we did, I would create an app.js file and add a script tag for it in base.html.twig. Then, on certain pages, you might also need to include some page-specific JavaScript, like to power a comment-voting feature that only lives on one page.

That's what I'm doing and that's why I created a file called question_show.js: it's custom JavaScript for that page.

Inside question_show.js, I'm going to paste about 15 lines of code.

/**
* Simple (ugly) code to handle the comment vote up/down
*/
var $container = $('.js-vote-arrows');
$container.find('a').on('click', function(e) {
e.preventDefault();
var $link = $(e.currentTarget);
$.ajax({
url: '/comments/10/vote/'+$link.data('direction'),
method: 'POST'
}).then(function(response) {
$container.find('.js-vote-total').text(response.votes);
});
});

This finds the .js-vote-arrows element - which we added here - finds any a tags inside, and registers a click listener on them. On click, we make an AJAX request to /comments/10 - the 10 is hardcoded for now - /vote/ and then we read the data-direction attribute off of the anchor element to know if this is an up vote or down vote. On success, jQuery passes us the JSON data from our endpoint. Let's rename that variable to data to be more accurate.

... lines 1 - 4
$container.find('a').on('click', function(e) {
... lines 6 - 8
$.ajax({
... lines 10 - 11
}).then(function(data) {
$container.find('.js-vote-total').text(data.votes);
});
});

Then we use the votes field from the data - because in our controller we're returning a votes key - to update the vote total.

Overriding the javascripts Block

So... how do we include this file? If we wanted to include this on every page, it would be pretty easy: add another script tag below jQuery in base.html.twig. But we want to include this only on the show page. This is where having the jQuery script tag inside of a javascripts block is handy. Because, in a "child" template, we can override that block.

Check it out: in show.html.twig, it doesn't matter where - but let's go to the bottom, say {% block javascripts %} {% endblock %}. Inside, add a <script> tag with src="". Oh, we need to remember to use the asset() function. But... PhpStorm is suggesting js/question_show.js. Select that. Nice! It added the asset() function for us.

... lines 1 - 59
{% block javascripts %}
... lines 61 - 62
<script src="{{ asset('js/question_show.js') }}"></script>
{% endblock %}

If we stopped now, this would literally override the javascripts block of base.html.twig. So, jQuery would not be included on the page. Instead of overriding the block, what we really want to do is add to it! In the final HTML, we want our new script tag to go right below jQuery.

How can we do this? Above our script tag, say {{ parent() }}.

... lines 1 - 59
{% block javascripts %}
{{ parent() }}
<script src="{{ asset('js/question_show.js') }}"></script>
{% endblock %}

I love that! The parent() function gets the content of the parent block, and prints it.

Let's try this! Refresh and... click up. It updates! And if we hit down, we see a really low number.

AJAX Requests on the Profiler

Oh, and see this number "6" down on the web debug toolbar? This is really cool. Refresh the page. Notice that the icon is not down here. But as soon as our page makes an AJAX requests, it shows up! Yep, the web debug toolbar detects AJAX requests and lists them here. The best part is that you can use this to jump into the profiler for any of these requests! I'll right click and open this "down" vote link in a new tab.

This is the full profiler for that request in all its glory. If you use dump() somewhere in your code, the dumped variable for that AJAX requests will be here. And later, a database section will be here. This is a killer feature.

Next, let's tighten up our API endpoint: we shouldn't be able to make a GET request to it - like loading it in our browser. And... do we have anything that validates that the {direction} wildcard in the URL is either up or down but nothing else? Not yet.

Leave a comment!

26
Login or Register to join the conversation
Maxime M. Avatar
Maxime M. Avatar Maxime M. | posted 1 year ago

Hello, I have a problem with the code, when I click the buttons I get a Uncaught SyntaxError: Cannot use import statement outside a module at line 9 in question_show.js (renamed it), it corresponds to this line : import '../css/app.css';. I am using Xampp

Kinda lost here

Reply

Hey @Maxenor

The file question_show.js lives in the public directory and it's just not possible to import other files, Browsers does not work that way. You need to user Symfony Encore or something similar to manage your JS assets
I recommend you to watch our course about it: https://symfonycasts.com/sc...

Cheers!

Reply
Maxime M. Avatar

POST http://127.0.0.1/comments/10/vote/up 404 (Not Found). So I've found in the javascript file that if I write url: '/comments/10/vote/'+$link.data('direction'), the url is the one.. I have no clue where and how to fix this, I have copied the code from the site.

Reply

That's odd. The first thing I would do is to double-check that the route exist by running bin/console debug:router.
If the route exists, then, it's possible that the comment with id = 10 does not exist in your database. And finally, if that's not the case, perhaps the "up" direction is not allowed?

Cheers!

Reply
Default user avatar

Hi, i have a problem with buttons. I have status 404 for every AJAX request.

No route found for "POST /comments/10/vote/down" (from "https://localhost:8000/questions/reversing-a-spell")

Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException: "No route found for "POST /comments/10/vote/down" (from "https://localhost:8000/questions/reversing-a-spell")" at D:\hex\PhpstormProjects\symfonyCasts\vendor\symfony\http-kernel\EventListener\RouterListener.php line 136

UPD: it was mistake in @Route("/comments/{id}/vote/{direction}")

Reply
Antonio G. Avatar
Antonio G. Avatar Antonio G. | Artem | posted 7 months ago

Thanks for posting your solution! I was making the same exact mistake

1 Reply

Hey Artem!

Thanks for posting your fix - I'm glad you figured it out :).

Cheers!

Reply
Carlos J. Avatar
Carlos J. Avatar Carlos J. | posted 1 year ago

Hi, first of all thank you so much for this tutorial, i'm having a little issue, i see in your Ajax Request on console that every ajax is taking about 58 to 72ms , but in my project every ajax call that i do, is taking from 320 up to 520ms to do the request, this makes the voting kinda slow and i want to know if this is a common issue with Symfony.
I'm not following the tutorial folder code from github because i made my own project with the setup tutorials. The thing is that i don't know if i have to uninstall some dependencies to lower the ms on the call, thanks in advance!

Reply

Hey Shorsh

It's not easy to detect the bottle neck of your request here but at least I can list a few things to consider
- If you're on Windows or WSL it's normal
- The first time you make a request to your app (after changing a file) it's slower than usual but the subsequent request should be faster
- What webserver are you using?
- Try creating a new Symfony project and check how much time does it take to process a request

Cheers!

Reply
Carlos J. Avatar

Oh maybe it's the one about Windows, i'm using the Symfony Local Server v4.23.0.
Yeap, that's one of my issues actually, if i install a new project using any of the symfony commands on my cmd, it takes forever, like 15 more times than it takes on your computer, so i have to use the composer create-project symfony/website-skeleton my_project_name , because if i create a project with "symfony new my_project" it can take around 1 or 2 minutes to create it.

Reply

Yeah, Windows always causes troubles. Are you on WSL 2? because that version has an horrible performance on folders under GIT

Reply
Carlos J. Avatar

Nope, i'm not using WSL, i just fixed my performance issue and went from 528ms to 202/212 which is really good.Installing OPCache and APCu made a huge impact on performance i guess, also setting up the realpath_cache_size = 4096k to that number as is specified in the performance guide helped a lot.
For windows users, just follow that guide and it will increase performance significantly.

In my case, i had to specify these 2 settings at the bottom of my php.ini to make it work.
First added these 2 lines so php can find the opcache.dll:

extension=php_gd2.dll
zend_extension=php_opcache.dll

second, i had to download APCu from the official website and dragged the .dll to my EXT directory inside PHP's folder, then, added this to my php.ini file:

[apcu]
extnsion=php_apcu.dll
apc.enabled=1
apc.shm_size=32M
apc.ttl=7200
apc.enable_cli=1
apc.serializer=php

1 Reply

Awesome! Thanks for sharing your findings Carlos J.

1 Reply
Oliver Avatar

Hi,

first of all thnaks for the great course. I have a problem though. When clicking up/down nothing happens. It seems there is a problem with the JS file. In the line "Unresolved function or method on() " the IDE tells me "Unresolved function or method on()". I also tried copy/paste the code from the lecture.

But no error in console or anything is thrown.

I'd be really thankful if someone could help me.

Reply

Hey Olli,

Did you download the our course code and start from the start/ directory? Or are you following this course on your own project?

If you have problems with calling that .on() method - most probably the object on which you're calling it is a different object. For jquery we use https://code.jquery.com/jqu... link, do you use the same link with the same version? If no, could you try to use the exact same version of jQuery we have in this screencast? Also, try to execute "console.log($)" in your question_show.js file and see if it's a jQuery object. Probably you need to call "jQuery" instead of "$" in your code. And please, make sure that you include this question_show.js file *after* the jquery include in the base layout and not before it. Does anything from this help?

Cheers!

Reply
Marie L. Avatar

Hi Olli!
I seem to have the same or similar problem. I tried to log the $container and it seems like it isn't found. Is yours empty too Olli?
Worked around this by putting everything in a function called on window load and searched for the element by class, but die click functionality is still not working.
Maybe this additional information helps that someone got a fixing idea.

Reply
Oliver Avatar

Yes, object with length 0

{
"length": 0,
"prevObject": {
"0": {
"location": {
"href": "http://127.0.0.1:8000/questions/Reversing-a-spell#",
"origin": "http://127.0.0.1:8000",
"protocol": "http:",
"host": "127.0.0.1:8000",
"hostname": "127.0.0.1",
"port": "8000",
"pathname": "/questions/Reversing-a-spell",
"search": "",
"hash": ""
}
},
"length": 1
}
}
Reply
Cahuzac R. Avatar
Cahuzac R. Avatar Cahuzac R. | posted 1 year ago

Hi,
I have a problem, I think the part of ajax is not executed. If I write an alert(), it doesn't work. how can I debug that please ?
This way the voteCount don't change :/
I have done exactly same as video ..

Reply

Hi Remi!

Do you see any errors in the console? If the code doesn't execute at all (assuming jQuery is installed), I'd say you might have a typo on the HTML, specifically the classes that jQuery uses to find the elements and assign the click event. (notice that for this to work, the wrapper div must have the class js-vote-arrows

But this is just a guess! You will have to check and debug your code to find where the issue really is!

Reply
Cahuzac R. Avatar
Cahuzac R. Avatar Cahuzac R. | shadowc | posted 1 year ago

Thank you for your answer, I just have wrote '_' instead of '-' in the class name in html<> ....
Your tutorial is very nice. Thanks for your time !

Reply
quynh-ngan_vu Avatar
quynh-ngan_vu Avatar quynh-ngan_vu | posted 2 years ago

Wonderful chapter! I also love the parent() function <3

Reply

Hey Ngan-Cat ,

Thank you for this feedback! Yeah, parent() function is pretty useful to add some content or create a wrapper :)

Cheers!

1 Reply
Sonali P. Avatar
Sonali P. Avatar Sonali P. | posted 2 years ago

Why i am getting error like Route not found?

I have done exactly same as video

Reply

Hey @Sonali!

Hmm, that's a good question! Let's see if we can figure it out :). Try running this at your terminal:


php bin/console debug:router

In the list, do you see the route that you're looking for? If not, then there is somehow a problem with your @Route annotation. If you *do* see it on the list, does the URL match what you're putting in the browser?

Let me know! I'm sure it's something minor :).

Cheers!

Reply
Default user avatar
Default user avatar Thomas Bisson | posted 2 years ago

I don't know why for you the 6 at the end of the vote disappear. For me there is always something like 06 or 856... How can your 6 disappear when it's still in the twig file ?

Reply

Hey Thomas Bisson

The value of votes changes dynamically. I believe there's something funny on your Javascript code. You should replace the content of the element with the new votes total

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.9
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}