The code side of life

Angular 2, Webpack, SASS and ViewEncapsulation

Introduction

user

Shlomi Assaf


Angular 2 SASS Webpack ViewEncapsulation

Angular 2, Webpack, SASS and ViewEncapsulation

Posted by Shlomi Assaf on .
Featured

Angular 2 SASS Webpack ViewEncapsulation

Angular 2, Webpack, SASS and ViewEncapsulation

Posted by Shlomi Assaf on .

When things were simple

Working with webpack and SASS is no new news, the process is straight forward.
Set a loader to catch all .scss flies and define a chain of loaders to handle it.

Here's a simple example of a loader that compiles all SCSS files into CSS, process the CSS (images, relative imports etc...) and make's sure that the CSS output will go to the <head> tag on application boot.

{   
  test: /\.scss$/,      
  loaders: ['style-loader', 'css-loader', 'sass-loader']
}

Nothing new, it's the same process for AngularJS, react and any other application bundled with Webpack. That is... except Angular 2.

Closure my CSS

In angular 2 there is an interesting feature called ViewEncapsulation.

If you never heard the term mark it on your TODO list, it's a must, you'll find yourself breaking monitors trying to understand why your CSS selectors didn't hit.

I won't go into much detail, but in short ViewEncapsulation is configuration mode telling angular how to process CSS. Up until now you've probably used the None mode in your non-angular2 apps. In angular 2 you're using Emulated mode (the default) which means that angular will emulate ShadowDOM behaviour which is both super cool and future cool.
ShadowDOM means that every component has a CSS context which is hidden from the outside world, like if you create a var in a function, its only visible to that function. Angular 2 emulates that by settings special, random, unique attributes for every element that interacts with CSS which is in the styles or styleUrls component meta property.

So, if you define a component:

@Component({
  selector: 'app',
  encapsulation: ViewEncapsulation.Emulated, // not needed - default value
  styles: [ `div { background: red; width: 100px; height: 100px;` ],
  template: `<div></div>`
})
class MyComponent {}  

The DOM will look something like this:

<app _nghost-vsf-8>  
  <div _ngcontent-vsf-4></div>
</app>  

Notice the 2 extra attributes? they create the emulation.

The CSS code injected into the <head> tag looks something like this:

<head>  
  <style>
    div[_ngcontent-vsf-4] { background: red; width: 100px; height: 100px; }
  </style>
</head>  

What does this mean? it means that we actually created a red square out of every div but since we are in emulation mode angular made sure only div's within out components template will actually have this rules applied.

This is very powerful, no more CSS chasing. But, if you don't know ViewEncapsulation you will get frustrated.

You've probably noticed the _nghostXXXXX attribute on our component's host element. This is not an error but host rules are out of the scope of this post.

Harmony, working with shadowed and global CSS rules

In your application you will face situations where you need to use global CSS rules. This can happen when a component is used in multiple places, especially when a parent component is also in emulation mode.

If you're CSS is set globally it will hit but it might not win since shadowed selectors are probably more specific.

If you're using webpack, you'll want to load global CSS outside of the styles meta property, in a static place usually the top of the file.
Remember our loader configuration?

{   
  test: /\.scss$/,      
  loaders: ['style-loader', 'css-loader', 'sass-loader']
}

This is good for loading global resources, it won't work when applying it on the styles meta property. Why? because the chain of loaders ends with the style-loader which actually returns a function to run on bootstrapping. Angular 2 expects a string.

To solve that we need to change our loader:

{   
  test: /\.scss$/,
  loaders: ['raw-loader', 'sass-loader']
}

That's better, now angular will get a compiled CSS in a string format.

But now, what about loading global SCSS files?

One solution is to configure the loader chain ad-hook on every requires:

require('!!style!css!sass!./my-styles.scss');  

Don't forget the !! at the start, to override the original chain.

This will work, if you have 1-2 main global CSS files. If you have more it's a lot of boilerplate. If your'e sticking to good angular standards it's better to keep global css rules of a component next to it.

Here's another solution:

{
  test: /\.scss$/,
  exclude: [/\.global\.scss$/],
  loaders: ['raw-loader', 'sass-loader']
},
{   
  test: /\.global\.scss$/,
  loaders: ['style-loader', 'css-loader', 'sass-loader']
}

We define 2 loaders, the 1st one is for our component specific CSS, the CSS that we set in the styles component meta property.
The 2nd loader is for global CSS, which we will usually require at the top of our modules.

There is a convention here, a file that ends with .global.scss is loaded with a chain that will make sure it end's up in the <style> tag inside <head>.
A file NOT ending with .global.scss will end up as a CSS string, suitable for angular components.

Looking at our previous component example:

import { Component, ViewEncapsulation } from '@angular/core';

require('./app.component.global.scss');

@Component({
  selector: 'app',
  encapsulation: ViewEncapsulation.Emulated, // not needed - default value
  styles: [ require('./app.component.scss') ],
  template: `<div></div>`
})
class MyComponent {}  

Simple.

If you have another approach to tackle this or found an error, please share in the comments below.

Thank you for reading!

user

Shlomi Assaf