Feb
21

Using CSS Modules with Angular, TypeScript and Bootstrap

posted on 21 February 2018 in programming

Warning: Please consider that this post is over 5 years old and the content may no longer be relevant.

CSS is simultaneously both the simplest and hardest programming language at once. What could be simpler than selectors, properties and values? And yet how quickly can CSS become an overwhelming mess, paralyzing unsuspecting developers who are too afraid to change a style because it’s hard to find and test all it’s usages, so they just add another style to the mess.

CSS Modules aims to alleviate a lot of these problems by scoping styles to a specific component, so other page styles won’t conflict with your component and your styles won’t leak onto other areas of the page.

How it works

CSS Modules are created using a CSS post processor, like webpack’s css-loader, the processor reads all your local styles and gives them an anonymised name, then returns you a JavaScript map of the style names to anonymised names that you can use to render your HTML. For instance, it will take the following style

.text-left {
  text-align: left;
}

and rewrite it to something like this

.styles__text-left__1GS2g {
  text-align: left;
}

You can access the anonymised names by importing or requiring them from JavaScript

import styles from './styles.css';

document.querySelector('#some-element').classList.add(styles.textLeft);

Local vs. Global styles

CSS Modules doesn’t just limit you to local styles however, you can specify that a selector is a global style using the :global modifier.

:global(.btn) {
  background-color: blue;
}

Which means this style won’t be anonymised when the process runs. Depending on your CSS post processor it may assume your styles are local by default, so you have to use the global modifier for global styles, or it may assume styles are global by default and you need to opt-in to transforming the style names to local styles, in this case you can use the :local modifier.

:local(.my-local-style) {
  background-color: blue;
}

This mode can be very useful for transitioning to using CSS Modules, all your existing styles will remain as they are and only those you tag as local with be modified.

Using CSS Modules with LESS or SASS

CSS Modules can work seamlessly with LESS or SASS, simply combine the syntax and have the CSS Modules processor run after your LESS / SASS processor.

:global(.global-parent) {
  .local-child: {
    color: pink;
  }
}

Becomes

.global-parent .styles__local-child__3JSLq {
  color: pink;
}

How are they used in React

CSS Modules were born in the React community and so they work very well together. After importing the stylesheet simply use the style object where ever you would normally use a class.

import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.module.css';

const HelloWorld = ({ name }) => (
  <div className={styles.welcome}>Hello, {name || 'world'}</div>
);

HelloWorld.propTypes = {
  name: PropTypes.string
};

export default HelloWorld;

How are they used in AngularJS

Angular isn’t really much different to React, although we need to pass the styles object to the template via the controller or scope.

var angular = require('angular');
var styles = require('./styles.css');

var Controller = function() {
    this.styles = styles;
};

angular
    .module('pageup.hello-world', [])
    .component('pupHelloWorld', {
        template: '<div ng-class="$ctrl.styles.welcome">Hello, {{$ctrl.name || "world"}}</div>',
        bindings: {
            name: '<'
        },
        controller: Controller
    });

Importing styles in TypeScript

If you try to import a stylesheet in TypeScript you’ll get an error

TS2307: Cannot find module './styles.less'. 

TypeScript is expecting a definitions file for styles.less and because it can’t find one it’s complaining. There are 2 common approaches to resolve this, using webpack you can configure the typings-for-css-modules-loader which will generate a styles.less.d.ts to keep the TypeScript compiler happy. Alternatively you can use the older require syntax which TypeScript will happily read as an any type. I tend to prefer the latter.

How to write tests with CSS Modules

Often when working with front end tests we use CSS selectors to find and interact with elements, however if we’ve anonymised the CSS name, we’re not going to be able to find it.

this.element.find('.welcome')

There are a couple of approaches to fixing this, one is to avoid using CSS classes in your tests and instead use a custom data attribute, for example we’ve been using data-test-target

this.element.find('[data-test-target=welcome]')

An alternative approach is to reference the style file in the test, use the style name mappings to target the correct style.

const styles = require('components/hello-world/styles.css');

describe('Hello World component', function () {
    it('should greet the world given no name supplied', function () {
        var greeting = _element.find('.' + styles.welcome)
        expect(greeting.text()).to.equal('Hello, world');
    });
});

Using Bootstrap with CSS Modules

The simplest way to use Bootstrap with CSS Modules is simply to treat bootstrap as global, import the Bootstrap styles onto the page and you can mix them with your local styles

<div class="row">
    <div class="col-xs-10 {{$ctrl.styles.ruleHeading}}">
        <span>Rule</span>
    </div>
    <div class="col-xs-2 {{$ctrl.styles.criteriaClearAll}}">
        <span>Clear</span>
    </div>
</div>

But what if you want to use Bootstrap just for your app, but don’t want to conflict with other styles on the page? I recommend importing it as a separate stylesheet using CSS Modules

const styles = require('components/hello-world/styles.css');
const globalStyles = require('styles/global.css');

Where ` globalStyles` imports Bootstrap. An issue that this introduces is how do we customise a Bootstrap style within a specific component? We can’t simply reference the style name in our component stylesheet because the globalStyles are all anonymised.

/* component styles.css */
/* .btn doesn't exist, it's become .global__btn__1GS2g */
.btn {
  font-size: 1.2em;
}

In this case you can’t change bootstrap styles, you can only define a new style and apply it to the element. This constraint actually enforces better CSS, we don’t have to worry about the ways .btn has been customised, we can just see what other classes have been applied to our elements.

<a ng-class="[globalStyles.btn, styles.btn]" href="#" role="button">Home</a>