Wednesday, October 9, 2024

Importing Custom Less Styles at Run-time in Angular

The organization where I work employs a common library for individual clients’ styles. These override the defaults as part of project builds. More recently, things have become slightly more complicated as Angular components are also being sold as individual widgets. These do not follow the same build process as applications; rather than compile Less variables in advance, these receive all colors via @Input parameters. As such, Less files must be imported by the widget at run-time, compiled, and converted to JavaScript Objects whose values may be utilized to set application variables. As the principal investigator into the architectural design of that process, I am now in a position to share my findings with you, the reader. This tutorial will follow a similar path as did the Setting CSS Values from Less Variables in Angular article, but this time, we’ll be fetching a Less stylesheet to go along with the Less variables, allowing us to entirely separate theme styles from our widget component.

Introducing the Less Service

As we do more processing of Less files, it only makes sense to encapsulate all of this functionality within a service. Let’s call it less-service.ts and place it in a services folder:

Lss Variables tutorial

 

All of the code that we will be writing here today, with the exception of the very last code snippet, will go into our service.

Reading Multiple Files Using the Angular HttpClient

In the Setting CSS Values from Less Variables in Angular tutorial, we read the contents of the variables.less file, located in the src/assets folder of our project. This time around, we will also be accessing styles from the common-styles.less file. We can not compile the styles without the variables, so we should use the RxJS forkJoin() function to retrieve the contents of both files together. ForkJoin is ideal for working with observables that only emit one value, or when we only require the last value of each before completion. As it happens, the HttpClient’s get() method returns an Observable that can be passed directly to forkJoin():

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { forkJoin, ReplaySubject } from "rxjs";

@Injectable()
export class LessService {
  constructor(http: HttpClient) {
    forkJoin([
      http.get('./assets/variables.less', {responseType: 'text'}),
      http.get('./assets/common-styles.less', {responseType: 'text'})
    ])
      .subscribe(
        data => {
           // Process files here... 
        },
        err => {
          console.log(err);
        });
  }
}

To invoke the LessService constructor, all we need to do is inject our service into our AppComponent as a constructor argument, and Angular will take care of the rest!

import { LessService } from './services/less-service';

export class AppComponent {
  constructor(lessService: LessService) {
  }
}

Read: HTML, CSS, and JavaScript Tools and Libraries

How to Convert Less Variables to a JavaScript Object

To convert our variables to JavaScript, we will use the less-vars-to-js library, just as we did last time. The only difference is that the data object now consists of a two element array – one for each returned Observable. Hence, the contents of the variables.less file are contained in element zero, i.e., data[0]:

import less from 'less';
import lessToJs from 'less-vars-to-js';

  // ...
  .subscribe(
    data => {
    let vars: Object;
    try {
      vars = lessToJs(data[0]);
    } catch(err) {
      console.log(err);
      return;
    } 

Appending Compiles Less Styles as a New StyleSheet

To compile the Less files, we’ll again be using the official, stable version of the Less npm JS library. Then, we will activate the compiled CSS rules by appending them as a new stylesheet in the document head. The trick to making that work is to assign the sheet’s innerText to the cssObject’s css property, which holds the compiled rules as a string:

const options = { modifyVars: vars };
less.render(data[1], options)
  .then((cssObject) => {
    const sheet = document.createElement('style');
    sheet.innerText = cssObject.css;
    sheet.title = 'common-styles';
    const head = document.getElementsByTagName('head')[0];
    head.appendChild(sheet);
    // Set default colors...
  })
  .catch((reason) => console.error(reason));

At this point, the new styles (shown below) should be immediately picked up by the browser and reflected in the application.

.background {  
  background-color: #52acf0;
}
.hover-background {  
  background-color: #b0d9f8;
}
.focus-border-background {
  background-color: gray;
}

That being said, the default theme colors will not trickle down to the FeedComponent Widget because, as mentioned in the web development tutorial introduction, those are set via @Input parameters. As such, they will not be picked up unless the AppComponent overrides its own default values with those of the variables.less file:

Working with Less Variables

 

We can see in the above image that the backgroundColorInput value of ‘cyan’ is currently being passed to the FeedComponent, whereas the @BackgroundColor defined in variables.less is “rgb(82, 172, 240)”. Should the owners of the client application that hosts the FeedComponent widget choose to go with the default theme colors, as defined in the variables.less file, they can do so by reading them in from the compiled CSS rules. The LessService can facilitate that by mapping Less variables to their values and making the Map available to subscribing components:

private _variablesMap$ = new ReplaySubject<Map<string, string>>(1);

public get variablesMap$() {
  return this._variablesMap$.asObservable();
}

To access the generated color values, we can repeat the same process as above using some dummy rules where each rule corresponds to a color variable, like so:

BackgroundColor { color: "#52acf0"; } 
FocusBorderColor { color: "gray"; } 
HoverColor { color: "#b0d9f8"; } 

Here is the code that wraps each custom variable and its associated value inside a dummy rule:

let dummyCssRules = 
  Object.keys(vars) // Remove the '@' for the map key
    .map(key => key.substring(1) + ' { color: ' + key + '; }\n')
    .join('');

We can now render the rules into CSS using the less JS library and convert the CSS rules to a JS Object. For that step, we will use the css-to-js library’s convert() function, so that we can access the rule names and their color attribute. One way to create the variablesMap from the JS Object is to run its keys through the Array’s map() method as there is a Map constructor for an array of key/value pairs. Finally, we can broadcast the variablesMap to subscribers and call complete() to let them know that there will not be any more broadcasts:

import less from 'less';

// ... 
less.render(dummyCssRules, options)
  .then(cssObject => {
    const compiledCssObject: Object = convert(cssObject.css);   
    const variablesMap: Map<string, string> 
	  = new Map(
	    Object.entries(compiledCssObject)
		  .map(([key, rule]: [string, string]) => [key, rule['color']])
	    );
    // Broadcast the variablesMap to subscribers
    this._variablesMap$.next(variablesMap);
    this._variablesMap$.complete();
  })
  .catch((reason) => console.error(reason));

Read: Project Management Tools for Web Developers

Setting the FeedComponent Widget Colors with Less

In the AppComponent’s constructor, we can access the lessService parameter and subscribe to its public variablesMap$ getter. Each color has the same name as in the variables.less file, but without the ‘@’ symbol:

constructor(lessService: LessService) {
  lessService.variablesMap$
    .subscribe(variablesMap => {
      this.setColors(
        variablesMap.get('BackgroundColor'), 
        variablesMap.get('HoverColor'), 
        variablesMap.get('FocusBorderColor')
      );
    });
}

Now we can see the variables.less colors reflected in the demo app:

Feed Components and Less Variables

 

Final Thoughts on Importing Custom Less Styles at Run-time in Angular

In this web development tutorial, we learned how to import Less files at run-time, in order to set CSS rules and individual color attributes in Angular applications. An alternative approach would be to employ a webpack script that does all of this during project builds. The advantage of importing styles from the application is that it offers a lot more flexibility at run-time. The bottom line is that you’ve got choices, and that’s always a good thing.

Read more JavaScript and web development tutorials.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured