In this case, I added in the property generateScopedName the value [hash:base64:5] that means it has to use the hash algorithm base64 and the length of the hashing will be of 5 characters, more info here.
Now it's time to put a new rule to handle the .html files. Add it at the end of the array because the rules are executed from the last one to the first one and we do not want to override how Angular compiles the .html. We just want to change the css-module attribute in the .html files by the class with the hashed class name as value.
The loader will search for a file called posthtml.config.js.
Here in the posthtml.config.js I only use the posthtml-css-modules plugin and I set as parameter where is the json file that contains the the mapping between class names and hashed class names. For example, I know the file will be: component-name.component.html and I know that the .json file will be in the same folder, therefore, we just replace the .html for scss.json in the path name.
What about ngClass directive? How we can apply a hashed class depending on the evaluation of an expression?
Lodash has the function template that allow us to interpolate data properties in 'interpolate' delimiters (<%= =>). So, in the HTML files, where we use ngClass or className directive, we need to add a function to interpolate. I named that function as getHashedClass where the parameter is the class name we want to hash.
On build time, we replace <%= getHashedClass(`visible`) %> by the hashed value for the class name visible and the result will be:
This loader will be executed for every component.html file.
First, it will search the component.scss.json that contains the mapping between class names and hashed class names (it was created by the posthtml-css-modules). It will search it through a function I named importJson:
If that file exists and the component.html contains the word getHashedClass it will continue. If not, it will return the component.html as it is.
If the loader continues, it will execute the function modifyTemplateLodash, this function uses the template function of Lodash:
What we do here is defining the template function of Lodash: every time it finds <%= getHashedClass(`unhashedClass`) %> it has to replace it by the value in the json which has as key the value of the parameter unhashedClass.
As you can see in html-css-modules.loader.js there is a function named modifyTemplateManually. It exists because once I had a border case where the template function of Lodash throw an error trying to replace <%= getHashedClass()%> so at the end I added modifyTemplateManually. It does the same as the template function. It will only be executed if modifyTemplateLodash fails.
Finally, we need to add the new loader in the extra-webpack.config.js in order it will be executed for every html file:
That is how we use CSS Modules in an Angular application.
Now, let see CSS Modules implemented in a component before the build and after the build. For that, I created the InfoComponent:
As you can see in the info.component.html we use css-module attribute, getHashedClass in the ngClass and className directives and all the class names that we need in the info.component.html are defined in the info.component.scss.
After the build, the info.component.scss.json is created:
This is a screenshot of InfoComponent at run time:
As you can see the class names are hashed in the html and in the styles. Also, if I changed the value of the slide the colors of the text below will change:
I think the best way to reach to a conclusion about using CSS Modules in Angular is analyzing the scenarios I defined in the section "Why would you want to use CSS Modules with Angular?" at the beginning of this article.
1. Your web application is used as web-component or inside an iframe on other website and you want to prevent the host website to override your styles.
This was the reason why I used CSS modules in an Angular project. On my work we have a web application that is used as web-component on other website, however, the websites's styles were overriding some of the styles of my application. So, we decided to use CSS Modules in our application. It helped us to prevent the websites overrides our the styles on the rules that uses class names as selector. Example:
However, it didn't prevent the websites overrides ours styles on the rules that uses the html elements as selector:
I haven't found yet a solution that prevents (all the cases) the websites overrides on the styles of my application. I was recommended to use ViewEncapsulation.ShadowDom but I didn't try yet and it’s not available in all the browsers.
2. You use ViewEncapsulation.None for your styles so it will help you to prevent conflicts in the styles of your components.
In my opinion I think CSS Modules is a great solution if you use ViewEncapsulation.None in order to prevent conflicts in the styles of your components. Because, the other options (that at least I know) are: 1) use BEM pattern 2) be really careful on how you name your classes
I think CSS Modules with this approach is easier than using BEM pattern. On the other case, I believe you will spend too much time being careful and soon or late you will have conflicts.
3. You use external libraries that override your application styles and you want to prevent that.
Thankfully I never had that situation neither I tried to simulate that situation in order to see what it would happen (But I will do one day and I will update this part). I think it will happen a similar situation as it happened to me in reason 1, it will work to prevent overrides in the styles that uses class names selector but it will not work to prevent overrides in the styles that uses html element selectors.
Finally, if you think you that using CSS Modules with this approach will decrease your performance while you develop your Angular Applications because you will lose track of the source class name at runtime, let me told you that in the projects where I use CSS Modules while I am developing and testing I run the project without CSS Modules. Later when I finish developing, I run my project with CSS Modules in order to see that everything it's working as expected.
I leave the configuration to run the project with or without CSS Modules at the end of this article.
I hope you like this article and it would be helpful for you.
Also, thanks Lars for his help, feedback and reviewing this article.
Extra: How to run your application with or without CSS Modules?
It's easy, we just need to add a configuration in the angular.json to run a custom build that will extend the angular build and replace the css-module attribute by class and remove getHashedClass without replacing the the class names by the hashed class names.
I added angular-css-modules-plain. Also, take in count that angular-css-modules-plain will use ./extra-webpack-plain.config.js to extend the angular build.
So, let's create that extra-webpack-plain.config.js:
As you can see, we only extend the HTML rule in order to replace the css-module attribute by class and remove getHashedClass without replacing the class names by the hashed class names on the HTML files. We do not need to do anything in the style files because by default they aren't hashed.
Also, we created a custom loader html-plain-class-name.loader.js that is responsible for replacing the css-module attribute by class and remove getHashedClass:
This loader will search css-module attribute and replace it by class. Then it will search for the getHashedClass and replace it by the value of the parameter unhashedClass. In any case, if the modifyTemplateLodash fails we have the modifyTemplateManually that does the same, but without Lodash.
Finally, we need to add a command to run the project without CSS Modules in the package.json:
That's all. If we run npm run start:app the project will be executed with CSS Modules. On the other hand, if we run npm run start:app:plain the project will be executed without CSS Modules