18.

Webpack Encore: JavaScript Greatness

|

Share this awesome video!

|

Tip

The recipe now adds these 2 files in a slightly different location:

  • assets/app.js
  • assets/styles/app.css

But the purpose of each file is exactly the same.

Okay: here's how this whole thing works. The recipe added a new assets/ directory with a couple of example CSS and JS files. The app.js file basically just console.log()'s something:

15 lines | assets/js/app.js
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import '../css/app.css';
// Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
// import $ from 'jquery';
console.log('Hello Webpack Encore! Edit me in assets/js/app.js');

The app.css changes the background color to light gray:

4 lines | assets/css/app.css
body {
background-color: lightgray;
}

Webpack Encore is entirely configured by one file: webpack.config.js:

75 lines | webpack.config.js
var Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or sub-directory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry('app', './assets/js/app.js')
//.addEntry('page1', './assets/js/page1.js')
//.addEntry('page2', './assets/js/page2.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// enables @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = 3;
})
// enables Sass/SCSS support
//.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
// uncomment if you use API Platform Admin (composer req api-admin)
//.enableReactPreset()
//.addEntry('admin', './assets/js/admin.js')
;
module.exports = Encore.getWebpackConfig();

We won't talk much about this file - we'll save that for the Encore tutorial - but it's already configured to point at the app.js and app.css files: it knows that it needs to process them.

Running Encore

To execute Encore, find your terminal and run:

yarn watch

This is a shortcut for running yarn run encore dev --watch. What does this do? It reads those 2 files in assets/, does some processing on them, and outputs a built version of each inside a new public/build/ directory. Here is the "built" app.css file... and the built app.js file. If we ran Encore in production mode - which is just a different command - it would minimize the contents of each file.

Including the Built CSS and JS Files

There's a lot more cool stuff going on, but this is the basic idea: we code in the assets/ directory, but point to the built files in our templates.

For example, in base.html.twig, instead of pointing at the old app.css file, we want to point at the one in the build/ directory. That's simple enough, but the WebpackEncoreBundle has a shortcut to make it even easier: {{ encore_entry_link_tags() }} and pass this app, because that's the name of the source files - called an "entry" in Webpack land.

35 lines | templates/base.html.twig
// ... line 1
<html>
<head>
// ... lines 4 - 5
{% block stylesheets %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Spartan&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css" integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk=" crossorigin="anonymous" />
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
// ... lines 13 - 33
</html>

At the bottom, render the script tag with {{ encore_entry_script_tags('app') }}.

35 lines | templates/base.html.twig
// ... 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>
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>

Let's try it! Move over and refresh. Did it work? It did! The background color is gray... and if I bring up the console, there's the log:

Hello Webpack Encore!

If you look at the HTML source, there's nothing special going on: we have a normal link tag pointing to /build/app.css.

Moving our Code into Encore

Now that this is working, let's move our CSS into the new system. Open public/css/app.css, copy all of this, then right click and delete the file. Now open the new app.css inside assets/ and paste.

127 lines | assets/css/app.css
body {
font-family: spartan;
color: #444;
}
.jumbotron-img {
background: rgb(237,116,88);
background: linear-gradient(302deg, rgba(237,116,88,1) 16%, rgba(51,61,81,1) 97%);
color: #fff;
}
.q-container {
border-top-right-radius: .25rem;
border-top-left-radius: .25rem;
background-color: #efefee;
}
.q-container-show {
border-top-right-radius: .25rem;
border-top-left-radius: .25rem;
background-color: #ED7458 ;
}
.q-container img, .q-container-show img {
border: 2px solid #fff;
border-radius: 50%;
}
.q-display {
background: #fff;
border-radius: .25rem;
}
.q-title-show {
text-transform: uppercase;
font-size: 1.3rem;
color: #fff;
}
.q-title {
text-transform: uppercase;
color: #444;
}
.q-title:hover {
color: #2B2B2B;
}
.q-title h2 {
font-size: 1.3rem;
}
.q-display-response {
background: #333D51;
color: #fff;
}
.answer-link:hover .magic-wand {
transform: rotate(20deg);
}
.vote-arrows {
font-size: 1.5rem;
}
.vote-arrows span {
font-size: 1rem;
}
.vote-arrows a {
color: #444;
}
.vote-up:hover {
color: #3D9970;
}
.vote-down:hover {
color: #FF4136;
}
.btn-question {
color: #FFFFFF;
background-color: #ED7458;
border-color: #D45B3F;
}
.btn-question:hover,
.btn-question:focus,
.btn-question:active,
.btn-question.active,
.open .dropdown-toggle.btn-question {
color: #FFFFFF;
background-color: #D45B3F;
border-color: #D45B3F;
}
.btn-question:active,
.btn-question.active,
.open .dropdown-toggle.btn-question {
background-image: none;
}
.btn-question.disabled,
.btn-question[disabled],
fieldset[disabled] .btn-question,
.btn-question.disabled:hover,
.btn-question[disabled]:hover,
fieldset[disabled] .btn-question:hover,
.btn-question.disabled:focus,
.btn-question[disabled]:focus,
fieldset[disabled] .btn-question:focus,
.btn-question.disabled:active,
.btn-question[disabled]:active,
fieldset[disabled] .btn-question:active,
.btn-question.disabled.active,
.btn-question[disabled].active,
fieldset[disabled] .btn-question.active {
background-color: #ED7458;
border-color: #D45B3F;
}
.btn-question .badge {
color: #ED7458;
background-color: #FFFFFF;
}
footer {
background-color: #efefee;
}

As soon as I do that, when I refresh... it works! Our CSS is back! The reason is that - if you check your terminal - yarn watch is watching our files for changes. As soon as we modified the app.css file, it re-read that file and dumped a new version into the public/build directory. That's why we keep this running in the background.

Let's do the same thing for our custom JavaScript. Open question_show.js and, instead of having a page-specific JavaScript file - where we only include this on our "show" page - to keep things simple, I'm going to put this into the new app.js, which is loaded on every page.

29 lines | assets/js/app.js
// ... lines 1 - 13
/**
* 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(data) {
$container.find('.js-vote-total').text(data.votes);
});
});

Then go delete the public/js/ directory entirely... and public/css/. Also open up templates/question/show.html.twig and, at the bottom, remove the old script tag.

59 lines | templates/question/show.html.twig
{% extends 'base.html.twig' %}
{% block title %}Question: {{ question }}{% endblock %}
{% block body %}
// ... lines 6 - 57
{% endblock %}

With any luck, Encore already rebuilt my app.js. So if we click to view a question - I'll refresh just to be safe - and... click the vote icons. Yes! This still works.

Installing & Importing Outside Libraries (jQuery)

Now that we're using Encore, there are some really cool things we can do. Here's one: instead of linking to a CDN or downloading jQuery directly into our project and committing it, we can require jQuery and install it into our node_modules/ directory... which is exactly how we're used to doing things in PHP: we install third-party libraries into vendor/ instead of downloading them manually.

To do that, open a new terminal tab and run:

yarn add jquery --dev

This is equivalent to the composer require command: it adds jquery to the package.json file and downloads it into node_modules/. The --dev part is not important.

Next, inside base.html.twig, remove jQuery entirely from the layout.

31 lines | templates/base.html.twig
// ... line 1
<html>
// ... lines 3 - 12
<body>
// ... lines 14 - 25
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>

If you go back to your browser and refresh the page now... it's totally broken:

$ is not defined

...coming from app.js. That makes sense: we did just download jQuery into our node_modules/ directory - you can find a directory here called jquery - but we're not using it yet.

How do we use it? Inside app.js, uncomment this import line: import $ from 'jquery'.

29 lines | assets/js/app.js
// ... lines 1 - 9
// Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
import $ from 'jquery';
// ... lines 14 - 29

This "loads" the jquery package we installed and assigns it to a $ variable. All these $ variables below are referencing the value we imported.

Here's the really cool part: without making any other changes, when we refresh, it works! Webpack noticed that we're importing jquery and automatically packaged it inside of the built app.js file. We import the stuff we need, and Webpack takes care of... packaging it all together.

Tip

Actually, Webpack splits the final files into multiple for efficiency. jQuery actually lives inside a different file in public/build/, though that doesn't matter!

Importing the Bootstrap CSS

We can do the same thing for the Bootstrap CSS. On the top of base.html.twig, delete the link tag for Bootstrap:

30 lines | templates/base.html.twig
// ... line 1
<html>
<head>
// ... lines 4 - 5
{% block stylesheets %}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Spartan&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css" integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk=" crossorigin="anonymous" />
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
// ... lines 12 - 28
</html>

No surprise, when we refresh, our site looks terrible.

To fix it, find your terminal and run:

yarn add bootstrap --dev

This downloads the bootstrap package into node_modules/. This package contains both JavaScript and CSS. We want to bring in the CSS.

To do that, open app.css and, at the top, use the good-old-fashioned @import syntax. Inside double quotes, say ~bootstrap:

129 lines | assets/css/app.css
@import "~bootstrap";
// ... lines 3 - 129

In CSS, this ~ is a special way to say that you want to load the CSS from a bootstrap package inside node_modules/.

Move over, refresh and... we are back! Webpack saw this import, grabbed the CSS from the bootstrap package, and included it in the final app.css file. How cool is that?

What Else can Encore Do?

This is just the start of what Webpack Encore can do. It can also minimize your files for production, supports Sass or LESS compiling, comes with React and Vue.js support, has automatic filename versioning and more. To go further, check out our free Webpack Encore tutorial.

And... that's it for this tutorial! Congratulations for sticking with me to the end! You already understand the most important parts of Symfony. In the next tutorial, we're going to unlock even more of your Symfony potential by uncovering the secrets of services. You'll be unstoppable.

As always, if you have questions, problems or have a really funny story - especially if it involves your cat - we would love to hear from you in the comments.

Alright friends - seeya next time!