If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeI love that I can put my styles right inside the component. In products.vue
, we render an element with a sidebar
class... and then immediately - without going anywhere else - we're able to add the CSS for that. We can still have external CSS files with shared CSS, but for any styling that's specific to a component, it can live right there.
But... we need to be careful. The class sidebar
is a pretty generic name. If we accidentally add a sidebar
class to any other component - or even to an element in our Twig layout - this CSS will affect it!
Find your browser, refresh, and open the HTML source. On top, here's our stylesheet: /build/products.css
. Open that up. Yep! That's what I expected: a .sidebar
class with the CSS from the component. It's easy to see that these styles would affect any element on the page with a sidebar
class... whether we want it to or not.
To solve this problem, Vue, well really, the CSS world - because this is a generic CSS problem - has created two solutions: scoped CSS and modular CSS. They're... two slightly different ways to solve the same problem and you can use either inside of Vue. We're going to use modular CSS.
What does that mean? It means that whenever we have a style
tag in a Vue component, we're going to include a special attribute called module
.
... lines 1 - 71 | |
<style lang="scss" module> | |
... lines 73 - 83 | |
</style> |
That's it. Back at your browser, leave the CSS file open, but close the HTML source and refresh the homepage. Ah! We lost our sidebar styling! It's a modular CSS "feature", I promise!
To see what's going on, go back to the tab with the CSS file and refresh. Woh. The class names changed: they're now .sidebar_
and then a random string. Back on the main tab, if we inspect element... the div
still has the normal sidebar
class.
Here's what's going on. When you add module
to the style
tag, Vue generates a random string that's specific to this component and adds that to the end of all class names. The great thing is that we can use generic class names like sidebar
without worrying about affecting other parts of our page. Because... in the final CSS, the class name is really sidebar_
then that random string.
Of course, now that this is happening, we can't just say class="sidebar"
in the template anymore. We need to somehow use the dynamic name - the one that includes the random string.
As soon as you add module
to a style
tag, Vue makes a new variable available in your template called $style
, which is a map from the original class name - like sidebar
- to the new dynamic name - like sidebar_abc123
.
I'm going to delete the p-3 mb-5
classes temporarily... just to simplify.
Ok: we no longer want to set the class
attribute to a simple string: it needs to be dynamic. And whenever an attribute needs to contain a dynamic value, we prefix it with :
, which is really v-bind
dressed up in its superhero costume.
Now, we're writing JavaScript. Use that new $style
variable to say $style.sidebar
.
<template> | |
... lines 2 - 4 | |
<div :class="$style.sidebar"> | |
... lines 6 - 53 | |
</template> | |
... lines 55 - 85 |
Unfortunately, at this time, the Vue plugin in PhpStorm doesn't understand the $style
variable, so it won't be much help here. But because we have a class called sidebar
inside the style
tag, we can say $style.sidebar
.
Let's try it! Move over, refresh and... we're back! Our class renders with the dynamic name.
Of course, it looks a little weird because our element is missing the two classes we removed. How can we add them back? We could do some ugly JavaScript, a plus, quote, space... but... come on! Vue almost always has a nicer way.
In fact, Vue has special treatment for the class
attribute: instead of setting it to a string, you can also pass it an array. Now we can include $style.sidebar
and the two static classes inside quotes: p-3
and mb-5
.
<template> | |
... lines 2 - 4 | |
<div :class="[$style.sidebar, 'p-3', 'mb-5']"> | |
... lines 6 - 53 | |
</template> | |
... lines 55 - 85 |
That should do it! Back at the browser... much better.
So... that's modular CSS! We're going to use it throughout the project and we'll learn a couple more tricks along the way.
You may have noticed that, when you look at the DOM, it's not super clear which component the sidebar
class is coming from: that random string doesn't tell us much. That's not a huge deal, but with a little config, we can make this friendlier in dev mode.
Open up webpack.config.js
. It doesn't matter where, but down here after enableVueLoader()
, I'm going to paste in some code. You can get this from the code block on this page.
... lines 1 - 8 | |
Encore | |
... lines 10 - 66 | |
// gives better module CSS naming in dev | |
.configureCssLoader((config) => { | |
if (!Encore.isProduction() && config.modules) { | |
config.modules.localIdentName = '[name]_[local]_[hash:base64:5]'; | |
} | |
}) | |
... lines 73 - 93 |
I admit, this is a bit low-level. Inside of Webpack, the css-loader
is what's responsible for understanding and processing styles. This localIdentName
is how the random string is generated when using modular CSS. This tell it to use the component name, then the class name - like sidebar
- and then a random hash. And we're only doing this in dev
mode because when we build for production, we don't care what our class names look like.
Tip
Actually, the [name]
part will be the filename of the Vue component, not
its name.
To make this take effect, at your terminal, hit Ctrl+C to stop Encore and then restart it:
yarn watch
Once that finishes, I'll move back to my browser. Refresh the CSS file first. Nice! The class name is products_sidebar_
random string. And when we try the real page, it works too.
Next, oh, we're going to try something that I'm so excited about. It's called "hot module replacement"... which is a pretty cool-sounding name for something even cooler: the ability to make a change to our Vue code and see it in our browser without - wait for it - reloading the page. Woh.
Hello,
I have a problem.
I'm trying to reproduce the work as shown but i'm facing an issue with the use of $style variable.
This is the error i'm having:
<blockquote>[Vue warn]: Property "$style" was accessed during render but is not defined on instance.
at <App>
[Vue warn]: Unhandled error during execution of render function
at <App>
Uncaught TypeError: Cannot read properties of undefined (reading 'sidebar')
</blockquote>
Here is my products.vue complete code
<template>
<div class="container-fluid">
<div class="row">
<aside class="col-xs-12 col-3">
<div :class="$style.sidebar">
<h5 class="text-center">
Categories
</h5>
<ul class="nav flex-column mb4">
<li class="nav-item">
<a
class="nav-link"
href="/"
>All Products</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#"
>Category A</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#"
>Category B</a>
</li>
</ul>
</div>
</aside>
<div class="col-xs-12 col-9">
<div class="row">
<div class="col-12">
<h1>
Products
</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-6 mb-2 pb-2">
TODO - load some products!
</div>
</div>
<div class="row">
<legend-component :title="legend" />
</div>
</div>
</div>
</div>
</template>
<script>
import LegendComponent from '../components/legend';
export default {
name: 'Products',
components: {
LegendComponent,
},
data() {
return {
legend: 'Shipping takes 10-12 weeks, and products probably won\'t work again',
};
},
};
</script>
<style lang="scss" module>
@import '../../scss/components/light-component';
.sidebar {
@include light-component;
ul {
li a:hover {
background: $blue-component-link-hover;
}
}
}
</style>
Hey @Kakashi!
Do the $style
variable becomes available IF you're using "modular" CSS. So, that's where I looked first. And I noticed you have module=""
. In the tutorial, it's just <style lang="scss" module>
. My first guess would be to tweak this part: I've never used module="", so I don't know if that actually works or not (it seems like it's not working!).
Cheers!
// package.json
{
"devDependencies": {
"@symfony/webpack-encore": "^0.30.0", // 0.30.2
"axios": "^0.19.2", // 0.19.2
"bootstrap": "^4.4.1", // 4.5.0
"core-js": "^3.0.0", // 3.6.5
"eslint": "^6.7.2", // 6.8.0
"eslint-config-airbnb-base": "^14.0.0", // 14.1.0
"eslint-plugin-import": "^2.19.1", // 2.20.2
"eslint-plugin-vue": "^6.0.1", // 6.2.2
"regenerator-runtime": "^0.13.2", // 0.13.5
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^8.0.0", // 8.0.2
"vue": "^2.6.11", // 2.6.11
"vue-loader": "^15.9.1", // 15.9.2
"vue-template-compiler": "^2.6.11", // 2.6.11
"webpack-notifier": "^1.6.0" // 1.8.0
}
}
If you have problems with the name part of the css class replace the config part in webpack.config.js with
It seems like the setting moved an level higher.
found in https://vue-loader.vuejs.org/guide/css-modules.html#usage
there you can see that modules and localIdentName are on the same level
Not testet in Vue3
EDIT: And don't forget to start yarn watch again ;)