07.

Require CSS!?

Share this awesome video!

|

Oh, before we talk about CSS, I forgot to mention that these public/build files do not need to be committed to your repository: we can rebuild them whenever we want from the source files. So inside .gitignore, add /public/build/* to make sure we don't commit them:

19 lines | .gitignore
// ... line 1
/public/build/*
// ... lines 3 - 19

Ok, onto something more important! Go to /login. Thanks to some JavaScript, if you type a really long username, a message pops up. The styling for this message comes from login.css. This is included in the template: login.html.twig:

// ... lines 1 - 4
{% block stylesheets %}
// ... lines 6 - 7
<link rel="stylesheet" href="{{ asset('assets/css/login.css') }}" />
{% endblock %}
{% block javascripts %}
// ... lines 12 - 13
<script src="{{ asset('build/login.js') }}"></script>
{% endblock %}
// ... lines 16 - 72

This makes sense: we have a script tag for login.js and also a link tag for login.css. But remember: I want you to start thinking about your JavaScript as an application... an application that can require its dependencies. And... isn't this CSS really a dependency of our app? I mean, if we forgot to include the CSS on this page, the application wouldn't break exactly... but it would look horrible! Honestly, it would look like I designed it.

What I'm saying is: wouldn't it be cool if we could require CSS from right inside our JavaScript?

Requiring CSS

Whelp... surprise! We can! Inside login.js, add require('../css/login.css'):

24 lines | public/assets/js/login.js
// ... lines 1 - 2
const $ = require('jquery');
require('../css/login.css');
// ... lines 5 - 24

We don't need to assign this to any variable.

So... what the heck does that do? Does that somehow magically embed that CSS onto the page? Well, it's not that magic. Look inside the build/ directory - you may need to right click and select "Synchronize" to update it. Woh! Suddenly, there is a new login.css file... which contains all of the stuff from our source login.css:

.wrapper {
margin-top: 80px;
margin-bottom: 20px;
}
.form-signin {
max-width: 420px;
padding: 30px 38px 66px;
margin: 0 auto;
background-color: #eee;
border: 3px dotted rgba(0,0,0,0.1);
}
.form-signin-heading {
text-align:center;
margin-bottom: 30px;
}
.form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
}
input[type="text"] {
margin-bottom: 0px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
input[type="password"] {
margin-bottom: 20px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.colorgraph {
height: 7px;
border-top: 0;
background: #c4e17f;
border-radius: 5px;
background-image: -webkit-linear-gradient(left, #c4e17f, #c4e17f 12.5%, #f7fdca 12.5%, #f7fdca 25%, #fecf71 25%, #fecf71 37.5%, #f0776c 37.5%, #f0776c 50%, #db9dbe 50%, #db9dbe 62.5%, #c49cde 62.5%, #c49cde 75%, #669ae1 75%, #669ae1 87.5%, #62c2e4 87.5%, #62c2e4);
background-image: -moz-linear-gradient(left, #c4e17f, #c4e17f 12.5%, #f7fdca 12.5%, #f7fdca 25%, #fecf71 25%, #fecf71 37.5%, #f0776c 37.5%, #f0776c 50%, #db9dbe 50%, #db9dbe 62.5%, #c49cde 62.5%, #c49cde 75%, #669ae1 75%, #669ae1 87.5%, #62c2e4 87.5%, #62c2e4);
background-image: -o-linear-gradient(left, #c4e17f, #c4e17f 12.5%, #f7fdca 12.5%, #f7fdca 25%, #fecf71 25%, #fecf71 37.5%, #f0776c 37.5%, #f0776c 50%, #db9dbe 50%, #db9dbe 62.5%, #c49cde 62.5%, #c49cde 75%, #669ae1 75%, #669ae1 87.5%, #62c2e4 87.5%, #62c2e4);
background-image: linear-gradient(to right, #c4e17f, #c4e17f 12.5%, #f7fdca 12.5%, #f7fdca 25%, #fecf71 25%, #fecf71 37.5%, #f0776c 37.5%, #f0776c 50%, #db9dbe 50%, #db9dbe 62.5%, #c49cde 62.5%, #c49cde 75%, #669ae1 75%, #669ae1 87.5%, #62c2e4 87.5%, #62c2e4);
}
.login-long-username-warning {
color: #8a6d3b;
background-color: #fcf8e3;
padding: 15px;
margin-bottom: 10px;
border: 1px solid #faebcc;
border-radius:4px;
}

Here's what's happening: we point Webpack at our login entry file: login.js:

21 lines | webpack.config.js
// ... lines 1 - 2
Encore
// ... lines 4 - 10
.addEntry('login', './public/assets/js/login.js')
// ... lines 12 - 16
;
// ... lines 18 - 21

Then, it parses all of the require statements inside:

24 lines | public/assets/js/login.js
// ... lines 1 - 2
const $ = require('jquery');
require('../css/login.css');
// ... lines 5 - 24

For any required JS files, it puts them in the final login.js. But it also parses the CSS files... and puts any CSS it finds into a final file - called login.css.

Actually, it's a bit confusing: the name login.css comes from the name of the entry: login:

21 lines | webpack.config.js
// ... lines 1 - 2
Encore
// ... lines 4 - 10
.addEntry('login', './public/assets/js/login.js')
// ... lines 12 - 16
;
// ... lines 18 - 21

Yep, each entry will cause one JavaScript file to be built and - if any of that JavaScript requires a CSS file - then it will also cause a CSS file to be created with the same name.

Of course, to use this in the template, we still need one link tag pointed to build/login.css:

// ... lines 1 - 4
{% block stylesheets %}
// ... lines 6 - 7
<link rel="stylesheet" href="{{ asset('build/login.css') }}" />
{% endblock %}
// ... lines 10 - 72

Let's try it - refresh! If you type a long name... it works! And... bonus time! When we talk about creating a production build later, this CSS file will automatically be minified.

Requiring the Layout CSS

So let's do this everywhere. Open layout.js and also the base layout: base.html.twig. Look at the top: we have a few css files, the first is main.css:

108 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
// ... lines 12 - 13
<link href="{{ asset('assets/css/main.css') }}" rel="stylesheet" />
{% endblock %}
// ... lines 16 - 17
</head>
// ... lines 19 - 106
</html>

In layout.js, require this: ../css/main.css:

10 lines | public/assets/js/layout.js
// ... lines 1 - 3
require('bootstrap');
require('../css/main.css');
// ... lines 6 - 10

As soon as we hit save, synchronize the build directory again... Yes! We have a new layout.css file! In base.html.twig, update the link tag to use this:

108 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
// ... lines 12 - 13
<link href="{{ asset('build/layout.css') }}" rel="stylesheet" />
{% endblock %}
// ... lines 16 - 17
</head>
// ... lines 19 - 106
</html>

Yep... everything still looks fine.

Handling Images

But... wait! Something amazing just happened! Look inside main.css. Woh! We're referencing a background image: ../images/dumbell-mini.png:

78 lines | public/assets/css/main.css
// ... lines 1 - 7
.mini-dumbbell {
// ... lines 9 - 10
background: url('../images/dumbbell-mini.png') center center no-repeat;
// ... line 12
}
// ... lines 14 - 78

That's a problem! Why? Because the final file lives in a completely different directory, so that ../ path will break!

Actually... it's not a problem! Webpack is amazing! It parses our CSS looking for any background images or fonts. When it finds one, it moves it into a build/images/ directory and rewrites the path inside the final CSS file to point there.

Tip

The file-loader has esModule: true by default since v5.0.0. If the generated URL looks like [object Module] - you will need to set esModule to false:

// webpack.config.js

Encore
    // ...
    .configureUrlLoader({
        images: {
            esModule: false
        }
    })
    // ...

The point is: all we need to do is write our CSS files correctly and... well... Webpack takes care of the rest!

Requiring Bootstrap & FontAwesome CSS

We're on a roll! There are two CSS files left in base.html.twig: Bootstrap and FontAwesome:

108 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous" />
// ... line 14
{% endblock %}
// ... lines 16 - 17
</head>
// ... lines 19 - 106
</html>

You know the drill: require this! Remove the Bootstrap link tag first:

108 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
// ... lines 13 - 14
{% endblock %}
// ... lines 16 - 17
</head>
// ... lines 19 - 106
</html>

In layout.js, above main.css, so that our CSS overrides their stuff, add require()... um... require... what? If we just require('bootstrap'), that will require the JavaScript file!

So... how can we include CSS files? Look in the node_modules/ directory... and scroll down to find bootstrap/. Ah, ok. Inside, there is a dist/ directory, then css/ and bootstrap.css.

A little bit of explanation: when you require the name of a module, Node reads a special key in that package's package.json file called main to figure out which file to actually require. But, if you want to require a specific file... just do it: bootstrap/dist/css/bootstrap.css:

11 lines | public/assets/js/layout.js
// ... lines 1 - 3
require('bootstrap');
require('bootstrap/dist/css/bootstrap.css');
require('../css/main.css');
// ... lines 7 - 11

This time, we don't need to make any other changes to base.html.twig: we already have a link tag for layout.css, which has everything we need:

107 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
// ... line 12
<link href="{{ asset('build/layout.css') }}" rel="stylesheet" />
{% endblock %}
// ... lines 15 - 16
</head>
// ... lines 18 - 105
</html>

To prove it, go back and refresh! It's still beautiful!

Yep, the built layout.css now has Bootstrap inside. And actually, Bootstrap itself references some fonts... and hey! There are now fonts in the build/ directory too! Those are handled just like background images.

Ok: one more CSS file to remove: FontAwesome. It's getting easy now! Remove the link tag from the layout:

107 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous" />
// ... line 13
{% endblock %}
// ... lines 15 - 16
</head>
// ... lines 18 - 105
</html>

Then, install that library:

yarn add font-awesome@4 --dev

I added @4 to make sure we get the version compatible with this project. Oh, and how did I know to use font-awesome as the exact library name? I cheated: I already used npms.io before recording to find it.

Back in layout.js, require font-awesome. Oh, but we need to find the exact file... in node_modules/font-awesome... ah! It looks like css/font-awesome.css - add that to the require:

12 lines | public/assets/js/layout.js
// ... lines 1 - 3
require('bootstrap');
require('bootstrap/dist/css/bootstrap.css');
require('font-awesome/css/font-awesome.css');
require('../css/main.css');
// ... lines 8 - 12

And Webpack is happy! Try it! Find the site and refresh! We still have our Bootstrap CSS and... yes! Our little user icon from FontAwesome is there! And on the homepage... yep! Those trash icons are from FontAwesome too!

Now, our base.html.twig file looks great! We have one CSS file and one JS file:

106 lines | templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
// ... lines 4 - 10
{% block stylesheets %}
<link href="{{ asset('build/layout.css') }}" rel="stylesheet" />
{% endblock %}
// ... lines 14 - 15
</head>
<body>
// ... lines 19 - 96
{% block javascripts %}
// ... lines 98 - 100
<script src="{{ asset('build/layout.js') }}"></script>
{% endblock %}
</body>
</html>

And all our dependencies are being required internally.