Dynamically Load CSS with the Angular CLI
I’ve written about lazy loading in the past, as well as how to handle various styling issues in Angular. This time we’ll explore how to customize the Angular CLI s.t. we can lazy load CSS files on the fly.
Normally if you generate an Angular app, you’ll get a styles.css
or styles.scss
file (based on whether you choose to use some CSS precompiler or not). This style gets directly compiled and injected into your index.html
. In fact, if you open the angular.json
file, you’ll find a configuration like
"dyncss": {
...
"build": {
...
options: {
...
"styles": [
"src/styles.scss",
],
}
},
...
},
and finally in your index.html
something like
<!doctype html>
<html lang="en">
<head>
...
<link rel="stylesheet" href="styles.11660fe9b6fe10ffd079.css"></head>
<body>
...
</body>
</html>
Now what if you have different styles you compile and you want to lazy load them at runtime?
TL;DR - Watch the lesson on Egghead
Create various style entry points
First of all, we need to create the various entry point CSS files of course. You can just create further scss
files at the level where the styles.scss
is in your project. For example:
client-a-styles.scss
client-b-styles.scss
You can see each of these as a fully independent styles.scss
but for different clients, different project themes, or whatever you’re trying to achieve. Obviously you may want to reuse parts among these via according Sass imports.
Configuring the CLI
Next we need to configure the Angular CLI to take our newly defined Sass files as well (btw, this totally also works with plain CSS of course).
"dyncss": {
...
"build": {
...
options: {
...
"styles": [
"src/styles.scss",
"src/client-a-styles.scss",
"src/client-b-styles.scss"
],
}
},
...
},
However, there’s a problem with this approach. Configuring the styles like this, all of them would be included and the last would win over the others. What do I mean by that. Well, most likely you might be using these for theming purposes, so client-a-styles.scss
might include something like
.app-title {
color: blue;
}
..while client-b-styles.scss
includes
.app-title {
color: red;
client-b-style.scss
is included after client-a-style.scss
, so it would override all previously defined CSS rules.
Hence, we need to tell the Angular CLI to NOT include these styles as we are going to load them on the fly. This can be done by including them as follows:
"dyncss": {
...
"build": {
...
options: {
...
"styles": [
"src/styles.scss",
{
"input": "src/client-a-styles.scss",
"bundleName": "client-a",
"inject": false
},
{
"input": "src/client-b-styles.scss",
"bundleName": "client-b",
"inject": false
}
],
}
},
...
},
Hint: you might have seen
lazy
being used when configuring styles in theangular.json
. It has been deprecated in favor ofinject
however, as the latter better expresses the actual meaning.
To see the effect, compile the app with ng build --prod
.
Lazy load at runtime
Now that we have separate CSS files being produced by the Angular CLI, we can think about how to lazy load them. In your app you should encapsulate the behavior in a dedicated service. For the purpose of showcasing it here, I simply create a loadStyle(...)
function in my AppComponent
and use the Document
to add the link to the <head>
section.
import { DOCUMENT } from '@angular/common';
...
export class AppComponent {
title = 'dyncss';
constructor(@Inject(DOCUMENT) private document: Document) {}
loadStyle(styleName: string) {
const head = this.document.getElementsByTagName('head')[0];
let themeLink = this.document.getElementById(
'client-theme'
) as HTMLLinkElement;
if (themeLink) {
themeLink.href = styleName;
} else {
const style = this.document.createElement('link');
style.id = 'client-theme';
style.rel = 'stylesheet';
style.href = `${styleName}`;
head.appendChild(style);
}
}
}
This would allow us to load the theme at runtime, based on the user login, some configuration variable or whatever, by simply invoking loadStyle('client-a.css')
.
Hint: Lazy load in development
If you try to invoke loadStyle('client-a.css')
in development (via ng serve
) you’ll most likely get the following error:
The reason is that the CSS is served via a JavaScript file (style.js
) as it is faster during development. To disable this behavior, you can add the "extractCss": true
to the project configuration in the angular.json
:
"dyncss": {
...
"build": {
...
options: {
...
"extractCss": true,
"styles": [
...
]
}
},
...
},
Alternative use case: During deployment
Alternatively you can “inject” the stylesheet during deployment into your index.html
via some deployment script (i.e. with Ansible). The script can post-process the index.html
during the deployment phase and add the correct stylesheet into the head section.
Conclusion
So in this article you learned
- how to configure the Angular CLI to not inject the styles into the HTML file
- how to lazy load the styles at runtime
Obviously the example is very simplified for the purpose of learning this approach. In a real world example you could have a much more elaborate and sophisticated mechanism for handling themes in your application.