What are CSS Modules?

There are some nice articles explaining what are CSS Modules, so I recommend you reading them first:

Why would you want to use CSS Modules with Angular?

CSS Modules are useful in the following scenarios:

  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.
  2. You use ViewEncapsulation.None for your styles so it will help you to prevent conflicts in the styles of your components.
  3. You use external libraries that override your application styles and you want to prevent that.

How to use CSS Modules with Angular?

We can use CSS Modules with Angular through postcss-modules and posthtml-css-modules.

First, postcss-modules hash all the class names in the styles files in build time.

Example: takes app.component.scss

.grid-container {
    display: grid; grid-template-rows: auto;
    grid-template-rows: 90px calc(100vh - 170px) 80px;
    width: 100vw; height: 100vh;
}

.header {
    background-color: #1ba0f7;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    img {
        width: 64px;
        height: 64px; 
    }
}

.footer {
    background-color: #1ba0f7;
}
app.component.scss

And it hashes all the classes like this:

._3Kuna {
    display: grid;
    grid-template-rows: auto;
    grid-template-rows: 90px calc(100vh - 170px) 80px;
    width: 100vw;
    height: 100vh;
}

._2-SC8 {
    background-color: #1ba0f7;
    display: flex;
    align-items: center;
    justify-content: flex-start;

    img {
        width: 64px;
        height: 64px;
    }
}

._2w5qX {
    background-color: #1ba0f7;
}
app.component.scss after using postcss-modules

Immediately, postcss-modules creates a .json file for each component style file where it stores the mapping between class names and hashed class names.

For example, the .json of app.component.scss is app.component.scss.json:

{
  "grid-container":"_3Kuna",
  "header":"_2-SC8",
  "footer":"_2w5qX"
}
app.component.scss.json

Then, posthtml-css-modules takes every  component.html file and changes the class names by their corresponding hashed class name.

In order to do that, we have to change the attributes class by css-modules, so posthtml-css-modules identify what name has to change.

For example, posthtml-css-modules will take app.component.html (you can see it has  css-modules as attribute instead of  class):

<div css-module="grid-container">
  <div css-module="header">
    <a href="https://angular.io/" target="_blank">
      <img src="assets/img/angular_logo.png">
    </a>
  </div>
  <div>
      <router-outlet></router-outlet>
  </div>
  <div css-module="footer">
  </div> 
</div>
app.component.html

And it will replace css-module for class but leave the hashed value as value of the class:

<div class="_3Kuna">
  <div class="_2-SC8">
    <a href="https://angular.io/" target="_blank">
      <img src="assets/img/angular_logo.png">
    </a>
  </div>
  <div>
      <router-outlet></router-outlet>
  </div>
  <div class="_2w5qX">
  </div>  
</div>
app.component.html after using posthtml-css-modules

That's it how we use CSS Modules in a Angular Project, now the question is:

How we use postcss-modules and posthtml-css-modules every time we run our project?

Thanks to @angular-builders/custom-webpack we can customize the build configuration of our Angular application.

First, we need to install the following packages:

npm install @angular-builders/custom-webpack 
postcss-modules posthtml-css-modules posthtml-loader raw-loader lodash -D

Then, we need to customize the Angular build through @angular-builders/custom-webpack, therefore, I created the file: extra-webpack.config.js in the root of my project:

const postcssModules = require('postcss-modules');
const path = require('path');
const AngularCompilerPlugin = require('@ngtools/webpack');

module.exports = (config, options) => {
    /*  SCSS EXTEND */
    const scssRule = config.module.rules.find(x => x.test.toString().includes('scss'));
    const postcssLoader = scssRule.use.find(x => x.loader === 'postcss-loader');
    const pluginFunc = postcssLoader.options.plugins;
    const newPluginFunc = function () {
        var plugs = pluginFunc.apply(this, arguments);
        plugs.splice(plugs.length - 1, 0, postcssModules({ generateScopedName: "[hash:base64:5]" }));
        return plugs;
    }
    postcssLoader.options.plugins = newPluginFunc;

    /*  HTML EXTEND */
    config.module.rules.unshift(
        {
            test: /\.html$/,
            use: [
                { loader: 'raw-loader' },
                {
                    loader: 'posthtml-loader',
                    options: {
                        config: {
                            path: './',
                            ctx: {
                                include: { ...options },
                                content: { ...options }
                            }
                        },
                    }
                },
            ]
        },
    );

    const index = config.plugins.findIndex(p => p instanceof AngularCompilerPlugin.AngularCompilerPlugin);
    const oldOptions = config.plugins[index]._options;
    oldOptions.directTemplateLoading = false;
    config.plugins.splice(index);
    config.plugins.push(new AngularCompilerPlugin.AngularCompilerPlugin(oldOptions));
    return config;
};
extra-webpack.config.js

Then, we have to modify the angular.json to indicate we want to customize the build:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-css-modules": {
      ...
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
                "path": "./extra-webpack.config.js"
            },
            ...
          },
          "configurations": {
            "production": {
              ...
            }
          }
        },
        "serve": {
          "builder": "@angular-builders/custom-webpack:dev-server",
          "options": {
            "browserTarget": "angular-css-modules:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "angular-css-modules:build:production"
            }
          }
        }
        ...
      }
    }},
  "defaultProject": "angular-css-modules"
}
angular.json

More information about how to customize the Angular build please see the @angular-builders/custom-webpack documentation.

Now, let's take a look at what we added in the extra-webpack.config.js.

First part:

const postcssModules = require('postcss-modules');
const path = require('path');
const AngularCompilerPlugin = require('@ngtools/webpack');

module.exports = (config, options) => {
    /*  SCSS EXTEND */
    const scssRule = config.module.rules.find(x => x.test.toString().includes('scss'));
    const postcssLoader = scssRule.use.find(x => x.loader === 'postcss-loader');
    const pluginFunc = postcssLoader.options.plugins;
    const newPluginFunc = function () {
        var plugs = pluginFunc.apply(this, arguments);
        plugs.splice(plugs.length - 1, 0, postcssModules({ generateScopedName: "[hash:base64:5]" }));
        return plugs;
    }
    postcssLoader.options.plugins = newPluginFunc;
    ...
};
extra-webpack.config.js

Here we search for the rules that apply for every .scss files (because I used scss extension for the styles in my project). Once we found it, we are going to search for the postcss-loader because postcss-modules is a plugin of postcss-loader. Then we add at the end of the plugins array of postcss-loader the postcss-modules plugin with our configuration.

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.

Second Part:

const postcssModules = require('postcss-modules');
const path = require('path');
const AngularCompilerPlugin = require('@ngtools/webpack');

module.exports = (config, options) => {
	...
    /*  HTML EXTEND */
    config.module.rules.unshift(
        {
            test: /\.html$/,
            use: [
                { loader: 'raw-loader' },
                {
                    loader: 'posthtml-loader',
                    options: {
                        config: {
                            path: './',
                            ctx: {
                                include: { ...options },
                                content: { ...options }
                            }
                        },
                    }
                },
            ]
        },
    );

    const index = config.plugins.findIndex(p => p instanceof AngularCompilerPlugin.AngularCompilerPlugin);
    const oldOptions = config.plugins[index]._options;
    oldOptions.directTemplateLoading = false;
    config.plugins.splice(index);
    config.plugins.push(new AngularCompilerPlugin.AngularCompilerPlugin(oldOptions));
    return config;
};
extra-webpack.config.js

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.

When posthtml-loader will be executed, it will execute plugins that are defined in the plugins property inside options or in a config file. Loader will find this file through the path property, like this:

                    options: {
                        config: {
                            path: './',
                            ctx: {
                                include: { ...options },
                                content: { ...options }
                            }
                        },
                    }

The loader will search for a file called posthtml.config.js.

module.exports = ({ file, options, env }) => {
	return ({
 		plugins: [
            require('posthtml-css-modules')(file.dirname.concat('/').concat(file.basename.replace('.html', '.scss.json')))
        ]
    })
};
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?

Neither postcss-modules nor posthtml-css-modules have a rule or configuration to apply CSS Modules on a ngClass directive. We can do it by ourselves using interpolation in the templates with Lodash and creating our own loader for the webpack HTML rule.

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.

Example:

<div [ngClass]="{'<%= getHashedClass(`visible`) %>': isVisible}">
</div>
example.component.html

On build time, we replace <%= getHashedClass(`visible`) %> by the hashed value for the class name visible and the result will be:

<div [ngClass]="{'_1WXX': isVisible}">
</div>
example.component.html

To do that, we need to create a loader for webpack (it's just a Javascript file) so we can replace all the <%= getHashedClass(`className`) %> by the corresponding hashed class name. For example, I created the loader in the file html-css-modules.loader.js in the folder scripts/loaders/ on the root of the project:

const lodash = require('lodash');
const loaderUtils = require('loader-utils');
const commonFunctions = require('../common-functions');

module.exports = function(source) {
    var options = loaderUtils.getOptions(this); // loader options
    var newTemplate = source; // Default response current html file
    try {
        // Read the Component Json File that contains the mapped classes names with hashed names.
        var componentJson = commonFunctions.importJson(options.file.replace('.html', '.scss.json'));
        // If the template includes the lodash function getHashedClass we should change it by the non hashed class name
        if (!componentJson || !newTemplate.includes('getHashedClass')) {
            return;
        }
        var replacerResult = modifyTemplateLodash(source, options.file, componentJson);
        if (!replacerResult) {
            replacerResult = modifyTemplateManually(source, options.file, componentJson);
            if (!replacerResult) {
                replacerResult = source;
            }
        }
        newTemplate = replacerResult;
    }
    catch(error) {
        console.log(`Fail on file: ${options.file} \n Error Details: + ${error}`);
    }
    finally {
        return newTemplate;
    }
}

function modifyTemplateLodash(source, file, json) {
    let newTemplate = source;
    try {
        if (!newTemplate || !json) {
            newTemplate = null;
            return;
        }
        // Function to lookup hashed class names
        let getHashedClass = function (unhashedClass) {
            return json[unhashedClass];
        }
        // Use lodash to template it, passing the class lookup function
        let compiled = lodash.template(newTemplate);
        newTemplate = compiled({
            getHashedClass: getHashedClass
        });
    }
    catch (error) {
        console.log(`Lodash Fail on file: ${file} \n Error Details: + ${error}`);
        newTemplate = null;
    }
    finally {
        return newTemplate;
    }
}

function modifyTemplateManually(source, file, json) {
    let newTemplate = source;
    try {
        if (!newTemplate || !json) {
            newTemplate = null;
            return;
        }
        // Get all the places where use getHashedClass
        const arrayClassNames = newTemplate.match(/<%=[ ]{0,}getHashedClass.*?\(([^)]*)\)[ ]{0,}%>/gi);
        let hashedClassName;
        let className;
        if (arrayClassNames) {
            for (let classToReplace of arrayClassNames) {
                className = classToReplace.replace(/<%=[ ]{0,}getHashedClass.*?\(/gi, '').replace(/\)[ ]{0,}%>/gi, '');
                if (className) {
                    className = className.replace(/`/gi, '').replace(/'/gi, '').replace(/"/gi, '');
                    hashedClassName = json[className];
                    if (hashedClassName) {
                        newTemplate = newTemplate.replace(classToReplace, hashedClassName);
                    }
                }
            }
        }
        return newTemplate;
    } catch (error) {
        console.log(`Html Manually Fail on file: ${file} \n Error Details: + ${error}`);
        newTemplate = null;
    }
    finally {
        return newTemplate;
    }
}
html-css-modules.loader.js

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:

const fs = require('fs');
const path = require('path');

function importJson(filePath) {
  try {
      if (!fs.existsSync(filePath)) {
          throw new Error(`Json file in path: ${filePath}, doesn't exists.`);
      }
      return JSON.parse(fs.readFileSync(filePath).toString());
  } catch (e) {
      throw new Error(`Fail getting json file in path: ${filePath}. Error: ${e}`);
  }
}

module.exports = {
  importJson
}
common-functions.js

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:

        ...
        // Function to lookup hashed class names
        let getHashedClass = function (unhashedClass) {
            return json[unhashedClass];
        }
        // Use lodash to template it, passing the class lookup function
        let compiled = lodash.template(newTemplate);
        newTemplate = compiled({
            getHashedClass: getHashedClass
        });
        ...
part of the code in modifyTemplateManually

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:

const postcssModules = require('postcss-modules');
const path = require('path');
const AngularCompilerPlugin = require('@ngtools/webpack');


module.exports = (config, options) => {

    /*  SCSS EXTEND */
    const scssRule = config.module.rules.find(x => x.test.toString().includes('scss'));
    const postcssLoader = scssRule.use.find(x => x.loader === 'postcss-loader');
    const pluginFunc = postcssLoader.options.plugins;
    const newPluginFunc = function () {
        var plugs = pluginFunc.apply(this, arguments);
        plugs.splice(plugs.length - 1, 0, postcssModules({ generateScopedName: "[hash:base64:5]" }));
        return plugs;
    }
    postcssLoader.options.plugins = newPluginFunc;

    /*  HTML EXTEND */

    config.module.rules.unshift(
        {
            test: /\.html$/,
            use:(info) => { 
                return [
                    { loader: 'raw-loader' },
                    {
                        loader: path.resolve('./scripts/loaders/html-css-modules.loader.js'),
                        options: {
                            file: info.resource
                        }
                    },
                    {
                        loader: 'posthtml-loader',
                        options: {
                            config: {
                                path: './',
                                ctx: {
                                    include: { ...options },
                                    content: { ...options }
                                }
                            },
                        }
                    },
                ]
            }
        },
    );

    const index = config.plugins.findIndex(p => p instanceof AngularCompilerPlugin.AngularCompilerPlugin);
    const oldOptions = config.plugins[index]._options;
    oldOptions.directTemplateLoading = false;
    config.plugins.splice(index);
    config.plugins.push(new AngularCompilerPlugin.AngularCompilerPlugin(oldOptions));
    return config;
};
extra-webpack.config.js

Resume

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:

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

@Component({
  selector: 'app-info',
  templateUrl: './info.component.html',
  styleUrls: ['./info.component.scss']
})
export class InfoComponent implements OnInit {
  public postcssPlugins: any[];
  public matSlideValue: boolean = false;
  constructor() {}

  ngOnInit() {}
}
info.component.ts
<div css-module="info-container">
    <div css-module="info-card">
        <a href="https://github.com/css-modules/css-modules" target="_blank">
            <img css-module="css-logo" src="assets/img/css-modules-logo.png">
        </a>
        <h3>What are CSS Modules?</h3>
        <p>
            CSS files in which all css-module names and animation names are scoped locally by default.
        </p>
    </div>
    <div css-module="info-card">
        <h3>How to implement CSS Modules in Angular?</h3>
        <p>
            Thanks to <a href="https://github.com/postcss/postcss-loader" target="_blank">postcss-loader</a> and the plugin <a href="https://github.com/css-modules/postcss-modules" target="_blank">postcss-modules</a>.<br>
            Also, thanks to <a href="https://github.com/posthtml/posthtml-loader" target="_blank">posthtml-loader</a> and the plugin <a href="https://github.com/posthtml/posthtml-css-modules" target="_blank">posthtml-css-modules</a>
        </p>
    </div>
    <div css-module="info-card">
        <h3>How to use CSS Modules in ngClass directive?</h3>
        <p>
            Like this: <mat-slide-toggle [(ngModel)]="matSlideValue">Slide me!</mat-slide-toggle>
        </p>
        <p>See the result:</p>
        <p [ngClass]="{'<%= getHashedClass(`slide-on`) %>': matSlideValue}">
            If Slide is On I will be blue
        </p>
        <p [ngClass]="{'<%= getHashedClass(`slide-off`) %>': !matSlideValue}">
            If Slide is Off I will be red
        </p>
        <p [className]="matSlideValue ? '<%= getHashedClass(`slide-on`) %>' : '<%= getHashedClass(`slide-off`) %>'">
            Slide On = blue and Slide off = red
        </p>
    </div>
</div>
info.component.html
@mixin card-border {
    padding: 5px 12px;
    margin-top: 10px;
    border-radius: 10px;
    box-shadow: 0 0 14px 0 rgba(0, 0, 14, 0.1);
}

.info-container {
    display: flex;
    flex-direction: column;
    padding: 0 10px;

    .info-card {
        @include card-border;
    }

    .css-logo {
        width: 64px;
        height: 64px;
    }
    
    .slide-on {
        color: #1ba0f7;
    }

    .slide-off {
        color: red;
    }
}
info.component.scss

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:

{
  "info-container": "_3E86Z",
  "info-card": "_1X6Xe",
  "css-logo": "_3O5Kn",
  "slide-on": "_1s_mr",
  "slide-off": "_2pkpl"
}
info.component.scss.json

This is a screenshot of InfoComponent at run time:

InfoComponent in 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:

Try it by yourself, here is the application running.

Also, here is the code of the Angular application.

Conclusion

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:

.my-class-name {
	background-color: red;
}

However, it didn't prevent the websites overrides ours styles on the rules that uses the html elements as selector:

div {
	background-color: red;
}

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.

If you are asking: "Why I would use ViewEncapsulation.None in my Angular application?" I recommend you to read the following article.

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.

That's all.

I hope you like this article and it would be helpful for you.

Here is the code of the Angular project that uses CSS Modules with this approach.

Thanks

I want to thanks Jeb for his support when I started using @angular-builders/custom-webpack and when I came up with more crazy questions.

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.

Currently, my angular.json is the following:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-css-modules": {
    ...
    }},
  "defaultProject": "angular-css-modules"
}

Now, we need to add a property in the project property:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-css-modules": {
       ...
    },
    "angular-css-modules-plain": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
                "path": "./extra-webpack-plain.config.js"
            },
            "outputPath": "dist/angular-css-modules",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
                ...
            }
          }
        },
        "serve": {
          "builder": "@angular-builders/custom-webpack:dev-server",
          "options": {
            "browserTarget": "angular-css-modules-plain:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "angular-css-modules-plain:build:production"
            }
          }
        },
        "lint": {
            ...
        }
      }
    },
  "defaultProject": "angular-css-modules"
}

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:

const path = require('path');
const AngularCompilerPlugin = require('@ngtools/webpack');

module.exports = (config, options) => {

    /*  HTML EXTEND */
    config.module.rules.unshift(
        {
            test: /\.html$/,
            use:(info) => {
                return [
                    { loader: 'raw-loader' },
                    {
                        loader: path.resolve('./scripts/loaders/html-plain-class-name.loader.js'),
                        options: {
                            file: info.resource
                        }
                    }
                ]
            }
        },
    );

    const index = config.plugins.findIndex(p => p instanceof AngularCompilerPlugin.AngularCompilerPlugin);
    const oldOptions = config.plugins[index]._options;
    oldOptions.directTemplateLoading = false;
    config.plugins.splice(index);
    config.plugins.push(new AngularCompilerPlugin.AngularCompilerPlugin(oldOptions));
    return config;
};
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:

var lodash = require('lodash');
var loaderUtils = require('loader-utils');

module.exports = function(source) {
    var options = loaderUtils.getOptions(this);
    var newTemplate = removeCssModulesAttribute(source, options.file);; // Default response current html file without css-module
    try {
        var replacerResult = modifyTemplateLodash(newTemplate, options.file);
        if (!replacerResult) {
            replacerResult = modifyTemplateManually(newTemplate, options.file);
            if (!replacerResult) {
                replacerResult = source;
            }
        }
        newTemplate = replacerResult;
    }
    catch(error) {
        console.log(`Fail on file: ${options.file} \n Error Details: + ${error}`);
    }
    finally {
        return newTemplate;
    }
}

function removeCssModulesAttribute(template, file) {
    try {
        if (!template) {
            return template;
        }
        return template.replace(/css-module/gi, "class");
    }
    catch (error) {
        console.log(`Fail on file: ${file} \n Error Details: + ${error}`);
        return template;
    }
}

function modifyTemplateLodash(source, file) {
    let newTemplate = source;
    try {
        if (!newTemplate) {
            newTemplate = null;
            return;
        }
        if (newTemplate.includes('getHashedClass')) {
            var getHashedClass = function (unhashedClass) {
                return unhashedClass;
            }
            var compiled = lodash.template(newTemplate);
            newTemplate = compiled({
                getHashedClass: getHashedClass
            });
        }
    }
    catch (error) {
        console.log(`Lodash Fail on file: ${file} \n Error Details: + ${error}`);
        newTemplate = null;
    }
    finally {
        return newTemplate;
    }
}

function modifyTemplateManually(source, file) {
    let newTemplate = source;
    try {
        if (!newTemplate) {
            newTemplate = null;
            return;
        }
        const arrayClassNames = newTemplate.match(/<%=[ ]{0,}getHashedClass.*?\(([^)]*)\)[ ]{0,}%>/gi);
        let className;
        if (arrayClassNames) {
            for (let classToReplace of arrayClassNames) {
                className = classToReplace.replace(/<%=[ ]{0,}getHashedClass.*?\(/gi, '').replace(/\)[ ]{0,}%>/gi, '');
                if (className) {
                    className = className.replace(/`/gi, '').replace(/'/gi, '').replace(/"/gi, '');
                    newTemplate = newTemplate.replace(classToReplace, className);
                }
            }
        }
        return newTemplate;
    } catch (error) {
        console.log(`Html Manually Fail on file: ${file} \n Error Details: + ${error}`);
        newTemplate = null;
    }
    finally {
        return newTemplate;
    }
}
html-plain-class-name.loader.js

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:

{
  "name": "angular-css-modules",
  "version": "1.0.0",
  "scripts": {
    "ng": "ng",
    "start": "node server/server.js",
    "start:app":"ng serve angular-css-modules",
    "start:app:plain":"ng serve angular-css-modules-plain",
    "build": "ng build angular-css-modules",
    "test": "ng test angular-css-modules",
    "lint": "ng lint angular-css-modules",
    "e2e": "ng e2e angular-css-modules",
  },
  ...
}
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

The code in the repo also includes this configuration to run the project with or without CSS Modules.