Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Better way of applying css-modules to AngularJS

I'm using css-modules with AngularJS 1.5 components now. The stack is webpack + css-loader?modules=true + angular 1.5 components + pug.

Currently I have to do the following steps to use css modules in my pug template.

// my-component.js

import template from 'my-component.pug';
import styles from 'my-component.css';

class MyComponent {
    constructor($element) {
        $element.addClass('myComponent');        // ------ (1)
        this.styles = styles;                    // ------ (2)
    }
}

angular.module(name, deps)
    .component('my-component', {
        controller: MyComponent,
        template: template,
    });

// my-component.pug
div(class={{ ::$ctrl.styles.fooBar }}) FooBar    // ----- (3)

// my-component.css
.myComponent { background: green; }
.fooBar { color: red; }

There are two problems:

  1. Every component has to inject $element and set its class name manually. The reason for doing this is, AngularJS component tag itself exists in the result HTML without any classes, which makes CSS difficult. For example, if I use MyComponent above like this:

    <div>
      <my-component></my-component>
    </div>
    

    it will generate the following HTML:

    <div>
      <my-component>
        <div class="my-component__fooBar__3B2xz">FooBar</div>
      </my-component>
    </div>
    

    Compared to ReactJS, <my-component> in above result HTML is an extra, sometimes it makes CSS difficult to write. So my solution is (1), to add a class to it.

  2. The class in template is too long (3). I know it is the correct way to reference $ctrl.styles.fooBar but this is way too long.

My ideal solution would be like this:

// my-component.js
angular.module(name, deps)
    .component('my-component', {
        controller: MyComponent,
        template: template,
        styles: styles,
    });

// my-component.css
div(css-class="fooBar") FooBar

The idea is to:

  1. make angular.module().component support an extra styles attribute, which will automatically do (2) this.styles = styles; in the controller, and apply (1) $element.addClass() as well.
  2. directive css-class to apply $ctrl.styles to element.

My question is, I have no idea how to implement idea 1 above (2 is easy). I appreciate if anyone could share some light on this.

like image 775
charlee Avatar asked Aug 31 '17 06:08

charlee


People also ask

Does angular use CSS modules?

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.

How would you implement CSS logic in an angular application?

Angular components can be styled via global CSS the same as any other element in your application. Simply drop a `<link>` element on your page (typically in index. html) and you're good to go! However, Angular additional gives developers more options for scoping your styles.

What is the advantage of using CSS modules compared to regular CSS?

CSS Modules let you write styles in CSS files but consume them as JavaScript objects for additional processing and safety. CSS Modules are very popular because they automatically make class and animation names unique so you don't have to worry about selector name collisions.


1 Answers

I came up with a solution which I don't quite satisfy with.

Angular component can accept a function as template and inject with $element. doc

If template is a function, then it is injected with the following locals:

  • $element - Current element
  • $attrs - Current attributes object for the element

Therefore we could attach the main class for component (.myComponent) in the template function, then regex replace all the occurance of class names with compiled class names.

// utils.js
function decorateTemplate(template, styles, className) {
    return ['$element', $element => {
        $element.addClass(styles[className]);
        return template.replace(/\$\{(\w+)\}/g, (match, p1) => styles[p1]);
    }];
}

// my-component.js
import style from './my-component.css';
import template from './my-component.pug';
import { decorateTemplate } from 'shared/utils';

class MyComponent {
    // NO NEED to inject $element in constructor
    // constructor($element) { ...
}

angular.module(name, deps)
    .component('myComponent', {
        // decorate the template with styles
        template: decorateTemplate(template, styles, 'myComponent'),
    });

// my-component.pug, with special '${className}' notation
div(class="${fooBar}") FooBar

There are still one place to be improved that decorateTemplate uses regex replacement and template has to use a special notation ${className} to specify the css-modules class names.

Any suggestions are welcome.

UPDATE

I updated my decorateTemplate() function to leverage pug features, so that local class names can be written as ._localClassName.

// utils.js
function decorateTemplate(template, styles, className) {

    return ['$element', ($element) => {
        $element.addClass(styles[className]);

        return template.replace(/\sclass="(.+?)"/g, (match, p1) => {
            let classes = p1.split(/\s+/);
            classes = classes.map(className => {
                if (className.startsWith('_')) {
                    let localClassName = className.slice(1);
                    if (styles[localClassName]) {
                        return styles[localClassName];
                    } else {
                        console.warn(`Warning: local class name ${className} not found`);
                        return className;
                    }

                } else {
                    return className;
                }
            });
            return ' class="' + classes.join(' ') + '"';
        });
    }];
}

// my-component.pug
._fooBar FooBar

Although this is much easier it does not eliminate the quirky notation (begin with _ for local class names) and regex replace.

Any suggestions are welcome.

like image 126
charlee Avatar answered Oct 27 '22 09:10

charlee