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

JavaScript & Page-Specific Assets

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.

The topic of API's is... ah ... a huge topic and hugely important these days. We're going to dive deep into API's in a future tutorial. But... I think we at least need to get to the basics right now.

So here's the goal: see this heart icon? I want the user to be able to click it to "like" the article. We're going to write some JavaScript that sends an AJAX request to an API endpoint. That endpoint will return the new number of likes, and we'll update the page. Well, the number of "likes" is just a fake number for now, but we can still get this entire system setup and working.

Creating the new JavaScript File

Oh, and by the way, if you look at the bottom of base.html.twig, our page does have jQuery, so we can use that:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
... lines 17 - 58
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
... lines 61 - 65
{% endblock %}
</body>
</html>

In the public/ directory, create a new js/ directory and a file inside called, how about, article_show.js. The idea is that we'll include this only on the article show page.

Start with a jQuery $(document).ready() block:

$(document).ready(function() {
... lines 2 - 9
});

Now, open show.html.twig and, scroll down a little. Ah! Here is the hardcoded number and heart link:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
... line 13
<div class="show-article-title-container d-inline-block pl-3 align-middle">
... lines 15 - 18
<span class="pl-2 article-details"> 5 <a href="#" class="fa fa-heart-o like-article"></a> </span>
</div>
</div>
</div>
... lines 23 - 94
</div>
</div>
</div>
</div>
{% endblock %}

Yep, we'll start the AJAX request when this link is clicked and update the "5" with the new number.

To set this up, let's make few changes. On the link, add a new class js-like-article. And to target the 5, add a span around it with js-like-article-count:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
... line 13
<div class="show-article-title-container d-inline-block pl-3 align-middle">
... lines 15 - 18
<span class="pl-2 article-details">
<span class="js-like-article-count">5</span>
<a href="#" class="fa fa-heart-o like-article js-like-article"></a>
</span>
</div>
</div>
</div>
... lines 26 - 97
</div>
</div>
</div>
</div>
{% endblock %}
... lines 104 - 107

We can use those to find the elements in JavaScript.

Copy the link's class. Let's write some very straightforward... but still awesome... JavaScript: find that element and, on click, call this function. Start with the classic e.preventDefault() so that the browser doesn't follow the link:

$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
e.preventDefault();
... lines 4 - 8
});
});

Next, set a $link variable to $(e.currentTarget):

$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
e.preventDefault();
var $link = $(e.currentTarget);
... lines 6 - 8
});
});

This is the link that was just clicked. I want to toggle that heart icon between being empty and full: do that with $link.toggleClass('fa-heart-o').toggleClass('fa-heart'):

$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
e.preventDefault();
var $link = $(e.currentTarget);
$link.toggleClass('fa-heart-o').toggleClass('fa-heart');
... lines 7 - 8
});
});

To update the count value, go copy the other class: js-like-article-count. Find it and set its HTML, for now, to TEST:

$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
e.preventDefault();
var $link = $(e.currentTarget);
$link.toggleClass('fa-heart-o').toggleClass('fa-heart');
$('.js-like-article-count').html('TEST');
});
});

Including Page-Specific JavaScript

Simple enough! All we need to do now is include this JS file on our page. Of course, in base.html.twig, we could add the script tag right at the bottom with the others:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
... lines 17 - 58
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script>
$('.dropdown-toggle').dropdown();
</script>
{% endblock %}
</body>
</html>

But... we don't really want to include this JavaScript file on every page, we only need it on the article show page.

But how can we do that? If we add it to the body block, then on the final page, it will appear too early - before even jQuery is included!

To add our new file at the bottom, we can override the javascripts block. Anywhere in show.html.twig, add {% block javascripts %} and {% endblock %}:

... lines 1 - 104
{% block javascripts %}
... line 106
{% endblock %}

Add the script tag with src="", start typing article_show, and auto-complete!

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

There is still a problem with this... and you might already see it. Refresh the page. Click and... it doesn't work!

Check out the console. Woh!

$ is not defined

That's not good! Check out the HTML source and scroll down towards the bottom. Yep, there is literally only one script tag on the page. That makes sense! When you override a block, you completely override that block! All the script tags from base.html.twig are gone!

Whoops! What we really want to do is append to the block, not replace it. How can we do that? Say {{ parent() }}:

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

This will print the parent template's block content first, and then we add our stuff. This is why we put CSS in a stylesheets block and JavaScript in a javascripts block.

Try it now! Refresh! And... it works! Next, let's create our API endpoint and hook this all together.

Leave a comment!

20
Login or Register to join the conversation

I rewrote like button script from jQuery to vanilla JS and FetchAPI:


document.addEventListener('DOMContentLoaded', function(){
    document.querySelector('.js-like-article').addEventListener('click', function(e){
        e.preventDefault();

        var anchor = e.currentTarget;
        anchor.classList.toggle('fa-heart-o');
        anchor.classList.toggle('fa-heart');

        fetch(anchor.href, {method: 'POST'})
            .then(function(response){
                return response.json();
            })
            .then(function(data){
                document.querySelector('.js-like-article-count').innerHTML = data.hearts;
            });
    });
});
1 Reply

Ha! That's nice! I haven't tested it but it looks solid. I'm wondering if you benchmark-ed its performance?

Cheers!

Reply
GDIBass Avatar
GDIBass Avatar GDIBass | posted 5 years ago

FYI you don't have to call toggle Class twice. This will toggle both classes:

$link.toggleClass('fa-heart-o fa-heart');

1 Reply

Hey GDIBass!

Another good tip! I actually didn't know this one :).

Cheers!

1 Reply
Florian Avatar
Florian Avatar Florian | posted 4 years ago | edited

It's also possible to write

var $link = $(this); <u>instead of</u> var $link = $(e.currentTarget);

My whole file at the end of the video looks like this:


// article_show.js
$(document).ready(function () {
    $('.js-like-article').on("click", function(e) {        
        var $link = $(this);
        $link.toggleClass('fa-heart-o fa-heart');

        $('.js-like-article-count').html('test');

        e.preventDefault();
    });
});
Reply

Hey Sven,

Yes, you're right :) But this might be tricky and sometimes you can easily miss in what spot what "this" means exactly. Also, with the new arrow function "this" does not work anymore :) So, using "$(e.currentTarget)" is vest practice todays.

Cheers!

Reply
Marco P. Avatar
Marco P. Avatar Marco P. | posted 4 years ago

Hello! I'm trying to deploy an application in a subfolder. I'm using a .htaccess generated by symfony/apache-pack. In this way everything works except the loading of the css and js files located in a /public/assets/css folder which are rewritten on www.dominio.it/assets/css/... instead of www.dominio.it/newfolder/ap...... how can I correct this? In the Twig templates I'm using the asset() function as suggested. Thanks for your help

Reply

Hey Marco P.

There is base-path: configuration variable under assets subkey of framework configuration. You can specify it in config/packages/assets.yaml

For more configuration variants see: https://symfony.com/doc/current/reference/configuration/framework.html#base-path

Cheers!

Reply
Default user avatar
Default user avatar Franc Woods | posted 4 years ago

my heart icon (fa fa-heart-o) never shows up... even before making the javascript additions. Am I missing something in moving the font or cs files?
UPDATE: found issue... there is an extra ' in the line where the font awesome css file is loaded!!

Reply

Hey Franc!

Thanks for getting back with the update! I see you found the solution, but I wonder was the problem on your side or in our code? I'm asking, because if it's in our code - we can fix it properly. Anyway, thanks for sharing your solution with others.

Cheers!

Reply
Default user avatar
Default user avatar Franc Woods | Victor | posted 4 years ago

My side... I think there was a copy / paste error when I was updating the link for the style sheet and the extra apostrophe was left in the link. I don't see it in the "final" solution for the base.html.twig template.

Reply

Hey Franc!

OK, so that's great, that means no changes needed for this screencast :p
Thanks again!

Cheers!

1 Reply
Dean D. Avatar
Dean D. Avatar Dean D. | posted 5 years ago

nope/
it isn't working. dang.... I can't find it.... I have reviewed, rewritten, and research many times. but it will not work. I really hate abandon this... It's wasting my time...
can anyone help?
Unexpected token "end of template" of value "" ("end of statement block" expected).
Exception {% block javascripts %} {{ parent() }} <script src="{{ asset('js/article_show.js') }}"></script>{% endblock %}

Reply

Hey Dean D.

Look's like you didn't close a "block" in your template (or maybe inside the template that you are extending), try double checking that all "blocks" are being closed. If you can upload your template file in somewhere I may be able to give it a look

Cheers!

Reply
Default user avatar

No subtitle here ? :(

Reply

Hey John!

Bah! Sorry about that - you found the one chapter where the subtitles failed (we're still perfecting a few formatting issues, so we allow subtitles to fail temporarily if we're not absolutely happy with them). We should have the subtitles up for this shortly!

Cheers!

Reply
Default user avatar
Default user avatar Emmanuel Abiola | posted 5 years ago

the love heart does not appear with 'TEST', after the heart icon is clicked on?

Reply

Hey Emmanuel Abiola

What error are you seeing? Remember that you have to fix the "js script tags" problem first

Cheers!

Reply
Default user avatar
Default user avatar Emmanuel Abiola | MolloKhan | posted 5 years ago

it works now! the error occurred from mis-spelling span around the no. 5 :)

Reply

Oh, no worries, that kind of errors happens all the time, but it would not if you were using PhpStorm ;)

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "sensio/framework-extra-bundle": "^5.1", // v5.1.3
        "symfony/asset": "^4.0", // v4.0.3
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.3
        "symfony/web-server-bundle": "^4.0", // v4.0.3
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "sensiolabs/security-checker": "^5.0", // v5.0.3
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.3
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.3
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.3
    }
}
userVoice