Page-Specific JS: Multiple Entries

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.

On the article show page, if you check the console... it's an error!

$ is undefined

Coming from article_show.js. This shouldn't be surprising. And not just because I seem to make a lot of mistakes. Open that template and go to the bottom. Ah, this brings in a js/article_show.js file:

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

Go find that: in public/, I'll close build/ and... there it is:

$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
e.preventDefault();
var $link = $(e.currentTarget);
$link.toggleClass('fa-heart-o').toggleClass('fa-heart');
$.ajax({
method: 'POST',
url: $link.attr('href')
}).done(function(data) {
$('.js-like-article-count').html(data.hearts);
})
});
});

This contains some traditional JavaScript from a previous tutorial. The problem is that the global $ variable doesn't exist anymore. If you look closely on this page, you'll see that, at the bottom, we include the app.js file first and then article_show.js. And, of course, the app.js file does import jQuery:

26 lines assets/js/app.js
... lines 1 - 10
import $ from 'jquery';
... lines 12 - 26

But as we learned, this does not create a global variable and local variables in Webpack don't "leak" beyond the file they're defined in.

So... this file is broken. And that's fine because I want to refactor it anyways to go through Encore so that we can properly import the variable on top.

Before we do that, let's organize one tiny thing. In assets/js, create a new components/ directory. Move get_nice_messages.js into that... and because that breaks our build... update the import statement in app.js to point here:

26 lines assets/js/app.js
... lines 1 - 14
import getNiceMessage from './components/get_nice_message';
... lines 16 - 26

Creating the Second Entry

Ok: I originally put this code into a separate file because it's only needed on the article show page. We could copy all of this, put it into app.js... and that would work! But sometimes, instead of having one big JavaScript file, you might want to split page-specific CSS and JavaScript into their own files.

To do that, we'll create a second Webpack "entry". Move article_show.js into assets/js/. Next, go into webpack.config.js and, up here, call addEntry() again. Name it article_show and point it at ./assets/js/article_show.js:

... lines 1 - 2
Encore
... lines 4 - 19
.addEntry('app', './assets/js/app.js')
.addEntry('article_show', './assets/js/article_show.js')
... lines 22 - 72
;
... lines 74 - 75

Now when we build Webpack, it will still load app.js, follow all the imports, and create app.js and app.css files. But now it will also load article_show.js, follow all of its imports and output new article_show.js and article_show.css files.

Each "entry", or "entry point" is like a standalone application that contains everything it needs.

And now that we have this new article_show entry, inside show.html.twig, instead of our manual <script> tag, use {{ encore_entry_script_tags('article_show') }}:

... lines 1 - 80
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('article_show') }}
{% endblock %}

I don't have a link tag anywhere... nope - it's not hiding on top either. That's ok, because, so far, article_show.js isn't importing any CSS. And so, Webpack is smart enough to not output an empty article_show.css file. But you could still plan ahead if you wanted: encore_entry_link_tags() will print nothing if there's no CSS file. So, no harm.

Ok: because we made a change to our webpack.config.js file, stop and restart Encore:

yarn watch

And... cool! The app entry caused these three files to be created... thanks to the split chunks stuff, and article_show just made article_show.js.

If you find your browser and refresh now... oh, same error... because we still haven't imported that. Back in article_show.js, import $ from 'jquery':

import $ from 'jquery';
$(document).ready(function() {
... lines 4 - 16
});

Refresh again and... boom! Error is gone. We can click the fancy JavaScript-powered heart icon.

Importing CSS

Because we haven't imported any CSS yet from article_show.js, we already saw that Webpack was smart enough to not output a CSS file. But! Open up _articles.scss. Part of this file is CSS for the article show page... which doesn't really need to be included on every page:

... lines 1 - 68
/* ARTICLE SHOW PAGE */
.show-article-container {
width: 100%;
background-color: #fff;
}
.show-article-container.show-article-container-border-green {
border-top: 3px solid green;
border-radius: 3px;
}
.show-article-img {
width: 250px;
height: auto;
border-radius: 5px;
}
.show-article-title {
font-size: 2em;
}
.like-article, .like-article:hover {
color: red;
text-decoration: none;
}
@media (max-width: 991px) {
.show-article-title {
font-size: 1.5em;
}
.show-article-title-container {
max-width: 220px;
}
}
.article-text {
margin-top: 20px;
}
.share-icons i {
font-size: 1.5em;
}
.comment-container {
max-width: 600px;
}
.comment-img {
width: 50px;
height: auto;
border: 1px solid darkgray;
}
.commenter-name {
font-weight: bold;
}
.comment-form {
min-width: 500px;
}
@media (max-width: 767px) {
.comment-form {
min-width: 260px;
}
.comment-container {
max-width: 280px;
}
}

Let's copy all of this code, remove it, and, at the root of the css/ directory, create a new file called article_show.scss and... paste!

/* ARTICLE SHOW PAGE */
.show-article-container {
width: 100%;
background-color: #fff;
}
.show-article-container.show-article-container-border-green {
border-top: 3px solid green;
border-radius: 3px;
}
.show-article-img {
width: 250px;
height: auto;
border-radius: 5px;
}
.show-article-title {
font-size: 2em;
}
.like-article, .like-article:hover {
color: red;
text-decoration: none;
}
@media (max-width: 991px) {
.show-article-title {
font-size: 1.5em;
}
.show-article-title-container {
max-width: 220px;
}
}
.article-text {
margin-top: 20px;
}
.share-icons i {
font-size: 1.5em;
}
.comment-container {
max-width: 600px;
}
.comment-img {
width: 50px;
height: auto;
border: 1px solid darkgray;
}
.commenter-name {
font-weight: bold;
}
.comment-form {
min-width: 500px;
}
@media (max-width: 767px) {
.comment-form {
min-width: 260px;
}
.comment-container {
max-width: 280px;
}
}

Both app.js and article_show.js are meant to import everything that's needed for the layout and for the article show page. app.scss and article_show.scss are kinda the same thing: they should import all the CSS that's needed for each spot.

At the top of article_show.scss, we don't strictly need to do this, but let's @import 'helper/variables to drive home the point that this is a standalone file that imports anything it needs:

@import './helper/variables';
/* ARTICLE SHOW PAGE */
... lines 4 - 74

Finally, back in article_show.js add import '../css/article_show.scss':

import '../css/article_show.scss';
import $ from 'jquery';
... lines 3 - 19

Ok, check your terminal! Suddenly, gasp! Webpack is outputting an article_show.css file! And wow! You can really see code splitting in action! That vendors~app~article_show.js probably contains jQuery, because Webpack saw that it's used by both entries and so isolated it into its own file so it could be re-used.

Anyways, back in show.html.twig copy the javascripts block, paste, rename it to stylesheets and then change to encore_entry_link_tags():

... lines 1 - 80
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('article_show') }}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('article_show') }}
{% endblock %}

That should do it! Move over, refresh and... cool! The page still looks good and the heart still works. If you inspect element on this page, in the head, we have two CSS files: app.css to power the layout and article_show.css to power this page.

At the bottom, we have 4 JavaScript files to power the two entrypoints. By the way, WebpackEncoreBundle is smart enough to not duplicate the vendors~app~article_show.js script tag just because both entries need it. Smart!

Next: we are close to having our whole app in Encore. Let's refactor a bunch more un-Webpack-ified code.

Leave a comment!

  • 2020-04-30 Victor Bocharsky

    Hey Virgile,

    Glad to hear it's clearer to you now! :)

    Cheers!

  • 2020-04-29 virgile sahaguian

    Hey Victor
    Thanks you for your explanation. You made this more clear for me :)

  • 2020-04-29 Victor Bocharsky

    Hey Virgile,

    Did you finish this course? You need to understand how Webpack Encore works. app.js is a special file that contains scripts that should be included on *every* page. But entries are specific, they contain JS code only for specific pages. Well, probably including 1 file instead of 2 is better, but I don't think it's a big difference on practice between 1 and 2 included files, another thing when we're talking about including 10 files :) And even with 1 or 2 files it still depends. A lot of people are working on optimizing Webpack work, and on practice including one gigantic unique file not always better than 2 but smaller. Anyway, Webpack is optimized by many people every day, and you get that optimization for free when using it.

    Cheers!

  • 2020-04-28 virgile sahaguian

    Hello Sf cast,

    Is it bad for performance to split the code into multiples files ? Isn't it better to import new JS into app.js ?
    Cheers!

  • 2019-09-19 Victor Bocharsky

    Hey Julien,

    I just double-checked the course for you and it works well, I don't have any missed migrations. Here's the way I tested it:
    1. Download and extract the course code
    2. Go to start/ directory
    3. Install Composer deps with: "composer install"
    4. Make sure I don't have the DB: "bin/console doctrine:database:drop --force"
    5. Create the DB: "bin/console doctrine:database:create"
    6. Run migrations: "bin/console doctrine:migration:migrate"
    7. Check that schema is valid: "bin/console doctrine:schema:validate"

    Actually, the same for finish/ directory. Please, make sure you execute these steps and if you still have invalid DB schema - you can run "bin/console doctrine:schema:update --dump-sql" to see what's exactly wrong, probably you just have a different MySQL version and it requires some specific queries to be executed, so you can just execute them with "bin/console doctrine:schema:update --force" and that's it.

    I hope this helps!

    Cheers!

  • 2019-09-19 Julien Bonnier

    Hello

    I feel like I'm missing some migrations. I kept watching the videos until this point anyway but I don't have articles nor space ice cream on the left side. Maybe this comes from other tutorials but I don't want to watch those. Is it possible to update the course code with all the content ?

    Cheers!

  • 2019-06-07 kaizoku

    Awesome ! Thank you, I'll check this out.

  • 2019-06-06 Vladimir Sadicov

    Hey kaizoku

    And the answer is YES! You just need to set them global inside your encore endpoint. You can find more information about it in this chapter https://symfonycasts.com/sc... or search official encore documentation.

    Cheers!

  • 2019-06-06 kaizoku

    Hi team,

    Is there a way to use Encore but still using the good old JS in the javascript Twig tag ?

    Example in a Twig template :


    {% block javascripts %}
    {{ parent() }}
    <script>
    $( document ).ready(function() {
    $('#htmlElement').html('Come on Ryan ! Are you really gonna make me create a specific .js file for this ?')
    $('#mytable').bootstrapTable({ })
    });
    </script>
    {% endblock %}