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

JavaScript, AJAX y el 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.

Este es nuestro próximo objetivo: escribir algo de JavaScript para que cuando hagamos click en los íconos de arriba o abajo, se realice un request AJAX a nuestra ruta JSON. Este "simula" guardar el voto en la base de datos y retorna el nuevo recuento de votos, el cual usaremos para actualizar el número de votos en la página.

Agregando Clases js- al Template

El template de esta página es: templates/question/show.html.twig. Para cada respuesta, tenemos estos links de votar-arriba y votar-abajo. Voy a agregar algunas clases a esta sección para ayudar a nuestro JavaScript. En el elemento vote-arrows, agrega una clase js-vote-arrows: lo usaremos en el JavaScript para encontrar el elemento. Luego, en el link de vote-up, agrega un atributo data llamado data-direction="up". Haz lo mismo para el link de abajo: data-direction="down". Esto nos ayudará a saber en cuál link se hizo click. Finalmente, rodea el numero de votos - el 6 - con un span que contenga otra clase: js-vote-total. Usaremos esto para encontrar el elemento para poder actualizar ese número.

... 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 %}

Agregando JavaScript Dentro del Bloque javascripts.

Para simplificar las cosas, el código JavaScript que escribiremos usará jQuery. De hecho, si tu sitio usa jQuery, probablemente querrás incluir jQuery en cada página... Lo cual significa que queremos agregar una etiqueta script a base.html.twig. En la parte de abajo, fíjate que tenemos un bloque llamado javascripts. Dentro de este bloque, voy a pegar una etiqueta <script> para descargar jQuery desde un CDN. Puedes copiar esto desde el bloque de código en esta página, o ir a jQuery para obtenerlo.

Tip

En los nuevos proyectos de Symfony, el bloque javascripts se encuentra en la parte superior de este archivo - dentro de la etiqueta <head>. Puedes dejar el bloque javascripts en <head> o moverlo aquí abajo. Si lo dejas dentro de head, asegúgate de agregar un atributo defer a cada etiqueta script: Esto hará que tu JavaScript sea ejecutado luego de que la página termine de cargar.

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

Si te preguntas por qué pusimos esto dentro del bloque javascripts... Más allá de que "parece" un lugar lógico, te mostraré por qué en un minuto. Ya que, técnicamente, si pusiéramos esto luego del bloque javascripts o antes, no habría ninguna diferencia por el momento. Pero ponerlos dentro va a ser útil pronto.

Para nuestro propio JavaScript, dentro del directorio public/, crea un nuevo directorio llamado js/. Y luego, un archivo: question_show.js.

Esta es la idea: usualmente tendrás algún código JavaScript que querrás incluir en cada página. No tenemos ninguno por el momento, pero si lo tuviéramos, yo crearía un archivo app.js y agregaría una etiqueta script para ello en base.html.twig. Luego, en ciertas páginas, podrías necesitar incluir algún JavaScript específico para la página, como por ejemplo, para hacer funcionar el voto de comentarios que solo vive en una página.

Esto es lo que estoy haciendo y esta es la razón por la que creé un archivo llamado question_show.js: Es JavaScript específico para esa página.

Dentro de question_show.js, voy a pegar al rededor de 15 líneas de código.

/**
* 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);
});
});

Esto encuentra el elemento .js-vote-arrows - el cual agregamos aquí - encuentra cualquier etiqueta dentro del mismo, y registra una función para el evento click allí. Al hacer click, hacemos una llamada AJAX a /comments/10 - el 10 es escrito a mano por ahora - /vote/ y luego leemos el atributo data-direction del elemento <a> para saber si este es un voto arriba o abajo. Al finalizar exitosamente, jQuery nos pasa los datos JSON de nuestra llamada. Renombremos esa variable a data para ser más exactos.

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

Luego usamos el campo votes de los datos - porque en nuestro controlador, estamos retornando una variable votes - para actualizar el total de votos.

Sobreescribiendo el Bloque javascripts.

Entonces... ¿Cómo incluimos este archivo? Si quisiéramos incluir esto en cada página, sería bastante fácil: agrega otra etiqueta script abajo de jQuery en base.html.twig. Pero queremos incluir esto solo en la página show. Aquí es donde tener el script de jQuery dentro del bloque javascripts es útil. Porque, en un template "hijo", podemos sobreescribir ese bloque.

Echemos un vistazo: en show.html.twig, no importa donde - pero vayamos al final, di {% block javascripts %} {% endblock %}. Dentro, agrega una etiqueta <script> con src="". Ah, tenemos que recordar usar la función asset(). Pero... PhpStorm nos sugiere js/question_show.js. Selecciona ese. ¡Muy bien! Agregó la función asset() por nosotros.

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

Si paráramos ahora, esto literalmente sobreescribiría el bloque javascripts de base.html.twig. Por lo que jQuery no sería incluido en la página. ¡En vez de sobreescribir el bloque, lo que realmente queremos es agregar algo a él! En el HTML final, queremos que nuestra nueva etiqueta script vaya justo debajo de jQuery.

¿Cómo podemos hacer esto? Sobre nuestra etiqueta script, di {{ parent() }}.

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

¡Me encanta! La función parent() toma el contenido del bloque padre, y lo imprime.

¡Probémoslo! Refresca y... Haz click en up. ¡Se actualiza! Y si hacemos click en down, vemos un número muy bajo.

Requests AJAX en el Profiler

Ah, y ¿Ves este número "6" aquí abajo en la barra de herramientas debug? Esto es genial. Refresca la página. Fíjate que el ícono no está aquí abajo. ¡Pero, tan pronto como nuestra página hace una llamada AJAX, aparece! Sip, la barra de herramientas debug detecta llamadas AJAX y las enlista aquí. ¡La mejor parte es que puedes usar esto para saltar al profiler para cualquiera de estos requests! Voy a hacer click con el botón derecho y abriré este link de voto "abajo" en una nueva pestaña.

Este es el profiler completo para la llamada en todo su esplendor. Si usas dump() en alguna parte de tu código, la variable volcada para esa llamada AJAX estará aquí. Y luego, tendremos una sección de base de datos aquí. Esta es una funcionalidad maravillosa.

A continuación, ajustemos nuestra ruta de la API: No deberíamos poder hacer un request GET al mismo - como si lo abriéramos en nuestro navegador. Y... ¿Tenemos algo que valide que el comodín {direction}... de la URL sea up o down pero nada más? Todavía no.

Leave a comment!

28
Login or Register to join the conversation
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}")

1 Reply
Antonio G. Avatar
Antonio G. Avatar Antonio G. | Artem | posted hace 1 año

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

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>

Reply

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!

1 Reply
Maxime M. Avatar
Maxime M. Avatar Maxime M. | posted hace 1 año

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/screencast/webpack-encore

Cheers!

Reply
Maxime M. Avatar
Maxime M. Avatar Maxime M. | Maxime M. | posted hace 1 año | edited

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
Carlos J. Avatar
Carlos J. Avatar Carlos J. | posted hace 2 años

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
Oliver Avatar Oliver | Marie L. | posted hace 2 años | edited

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 hace 2 años

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

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 hace 2 años | edited

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 hace 3 años

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 hace 3 años

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!

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