gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
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.
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 %} |
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.
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.
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.
I've had the problem that clicking the buttons did nothing. I had to alter the question_show.js
code to the following:
/**
* Simple (ugly) code to handle the comment vote up/down
*/
$(document).ready(function() {
$(".js-votes-arrows").find('a').on('click', function(e) {
e.preventDefault();
const link = $(e.currentTarget);
$.ajax({
url: '/comments/10/vote/'+link.data('direction'),
method: 'POST'
}).then(function(data) {
$(".js-votes-arrows").find('.js-vote-total').text(data.votes);
});
});
})
I also had to add defer
to the script tag since I had the {%javascript%}
at the beginning of the file in <head>
Hi @Brentspine!
Sorry for my slow reply! Yea, this makes sense - it's safer to wrap in $(document).ready(function() {
because it will "always" work vs mine, which depends on the placement of script tags, etc. These days, putting this logic inside of a Stimulus controller is EVEN better, but 👍 for what you have.
Cheers!
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
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/screencast/webpack-encore
Cheers!
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.
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!
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!
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!
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.
Yeah, Windows always causes troubles. Are you on WSL 2? because that version has an horrible performance on folders under GIT
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
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.
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!
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.
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
}
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 ..
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!
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 !
Hey Ngan-Cat ,
Thank you for this feedback! Yeah, parent() function is pretty useful to add some content or create a wrapper :)
Cheers!
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!
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 ?
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!
// 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
}
}
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}")