Angular: Setting angular.json to handle multiples themes, assets and environments during build time

Pedro Ribeiro Bastos Soares
5 min readAug 25, 2021

--

The Problem

Recently I convinced my team that a product’s frontend needed to be rebuilt from scratch, since there were many problems accumulated through the years and the design and UX was going to be updated anyway, It was a good opportunity to apply angular best practices.

One of the issues is that the code is used in 2 mostly equal products, their only difference is the CSS themes, angular material themes, assets, specific business logic in some pages and API calls.

Originally the app used the domain to check what product was the context, but this resulted in a lot of *ngIf’s, *ngClasses on the templates and if’s on components and services. It was hell to test locally because the context had to be forced programmatically every time. It also generated bugs because often the forced context was committed or developers forgot to test one of the products.

My solution was that the 2 products should not be treated with multiple if’s, CSS classes, etc. but during build time instead. So, this tutorial will show how to configure angular.json to use different themes, assets and environments depending on multiple contexts.

tl:dr

  • Create a file with a SASS function to define your theme structure.
  • Create one directory for each context, each directory have a file with the context theme. The theme file must have the same name on both context directory. Other SCSS files just need to import the theme file and use the variables.
  • Do the same with assets, create a directory for each context and use the same name for equivalent assets.
  • Create one environment for each context and use interfaces to force all of them to have the same properties.
  • Configure angular.json. Use styles and assets key to add SCSS files to the final bundle, stylePreprocessorOptions key to tell which directory contains the theme to be used, and fileReplacements to define which environment file to be used.
  • Consult this github project for details. https://github.com/pedroribeiro89/angular-multiple-themes

Project Themes and Material themes

First let’s use the SASS function to create the theme structure. In this example this structure is saved as _project_theme.scss file in the root directory.

@function project-theme($primary, $accent, $primary-font, $secondary-font) {
@return (
primary: $primary,
accent: $accent,
primary-font: $primary-font,
secondary-font: $secondary-font
);
}

Create a sub-directory for each context with a SCSS file inside. In this file you can define the theme for each context. In this example, the file is saved as _theme.scss. You may use a different file name, but it must be the same in each context.

@import 'src/styles/project_theme';
$project-theme: project-theme(
#605bff,
#ffb055,
unquote("Roboto, sans-serif"),
unquote("Nunito Sans, sans-serif")
) !default;

Do the same for angular material theme, just create a _material_theme.sccs inside each context directory.

Use styles.scss for global styles and also create a file for global styles for each context (in this example, c1.styles.scss and c2.styles.scss).

Now, configure angular.json to apply each context file to their corresponding environments.

Use the styles property to add the context global styles and the stylePreprocessorOptions property to tell angular which context folder to include (https://angular.io/guide/workspace-config#style-script-config)

"build": {
...
"configurations": {
"production1": {
...
"styles": [
"src/styles.scss",
"src/styles/context1/c1.styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles/context1"
]
},
...
},
"production2": {
...
"styles": [
"src/styles.scss",
"src/styles/context2/c2.styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles/context2"
]
},
...
},
"development1": {
...
"styles": [
"src/styles.scss",
"src/styles/context1/c1.styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles/context1"
]
},
...
},
"development2": {
...
"styles": [
"src/styles.scss",
"src/styles/context2/c2.styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles/context2"
]
},
...
}
},

Now import the theme file or material theme file on any SCSS you want to use it, check the styles.scss:

@import 'theme';
@import 'material_theme';
html, body { height: 100%; }
body {
margin: 0;
font-family: map-get($project-theme, primary-font);
}

Environment

Each context can have a set of environments. In this example development and production are used, but there could be more (like release, local etc). To ensure that all environments contain the same parameters, I used a typescript interface.

export interface IEnvironment {
production: boolean;
apiUrl: string;
contextId: number;
someAction: Function;
}

Example of a development environment:

export const environment: IEnvironment = {
production: false,
apiUrl: "http://localhost:3000/",
contextId: 1,
someAction: (param: string) => alert('I am on context 1' + param)
};

Change the environment configurations on the angular.json file and change the fileReplacements property to use the right file. Also change the serve property to use the right environment with ng serve.

"configurations": {
"production1": {
...
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
...
},
"production2": {
...
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment2.prod.ts"
}
],
...
},
"development1": {
...
},
"development2": {
...
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment2.ts"
}
],
...
}
},
...
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production1": {
"browserTarget": "angular-theme-env:build:production1"
},
"production2": {
"browserTarget": "angular-theme-env:build:production2"
},
"development1": {
"browserTarget": "angular-theme-env:build:development1"
},
"development2": {
"browserTarget": "angular-theme-env:build:development2"
}
},
"defaultConfiguration": "development1"
},

Assets

Assets are similar to themes, Create a directory for each context, make sure that analogous assets have the same name and configure the assets property on angular.json to include the directory folder and the context folder for each environment. (https://angular.io/guide/workspace-config#assets-configuration)

"configurations": {
"production1": {
...
"assets": [
"src/favicon.ico",
{
"glob": "**/*",
"input": "src/assets-context1/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "src/assets/",
"output": "/assets/"
}
]
},
"production2": {
...
"assets": [
"src/favicon.ico",
{
"glob": "**/*",
"input": "src/assets-context2/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "src/assets/",
"output": "/assets/"
}
]
},
"development1": {
...
"assets": [
"src/favicon.ico",
{
"glob": "**/*",
"input": "src/assets-context1/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "src/assets/",
"output": "/assets/"
}
]
},
"development2": {
...
"assets": [
"src/favicon.ico",
{
"glob": "**/*",
"input": "src/assets-context2/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "src/assets/",
"output": "/assets/"
}
]
}
}

With the previous config the right file will be in the final bundle and you can use it as if it was in the assets directory.

<img src="assets/dummylogo.png" width="100" height="36"/>

That’s it, I hope it can help you somehow. For the complete code you can check the project on: https://github.com/pedroribeiro89/angular-multiple-themes.

Extra tip: You can add the scripts options to package.json and make it easier to run/build the project:

"scripts": {
"context1": "ng serve",
"context2": "ng serve -c development2 --port 4201",
"build:context1": "ng build -c production1",
"build:context2": "ng build -c production2"
}

--

--