Angular change theme at runtime

Spread the love
  • 8
    Shares

Not everyone likes to see vibrant colors. In fact, changing color preferences has been present in Operating systems for a long time. Some users may prefer to have dark color combination, while others may go with light theme. Web applications are not that different too. With the rise in Web application in day-to-day agenda, the application developers must give a thought to customize application theme.

Colors and brush of different size
Colors and brush of different size.

Accidental Discovery

I was playing with few components from Angular Material site. I like the modularity thought coined by Angular Ecosystem. Though it is not mandatory, it definitely imbibes a good development practice.

I noticed that the Material site comes with default Angular Indigo-Blue theme. While the theme is quite soothing for the eyes, the site also offers an option to change theme via Theme Picker.

This was enough to start digging into the source code to find out how Material site is able to achieve the effect. While achieving a similar effect with a simple HTML / CSS site is pretty easy. You primarily need to

  • Create stylesheet files for each color combination (XXX.css)
  • Make sure the CSS structure remains same across all the CSS variants.
  • Query for “Link” tag which should be dynamically replaced.
  • Assign the new CSS reference to the DOM node located in above step.

Theme option Material way

Digging into the source code definitely helped me to understand how the whole magic works. There are few things we must follow. If you are to eager to know what we are going to do, here is the summarised list of activities.

  • Create Angular project using Angular CLI tools. Make sure the SCSS/SASS option is selected for styles
  • Create a folder “custom-themes” inside src/assets folder. This will contain various theme files.
  • Use scripts or node-sass from a command prompt to generate CSS for theme files.
  • Modify Angular.json to include each CSS file as a separate bundle.
  • Create a component to select themes of your choice.
  • (Optional)Create a component to store the theme preference. This is useful in case of page refresh and you wish to retain the settings.

Angular Project

You must be already aware of how to generate Angular project from command line. There is no fancy trick. Make sure that you have Angular CLI installed at a global location. It can be done using the following command at command prompt. For those who are not familiar with this command, it doesn’t matter which directory you are in. The -g parameter will do all the magic of installing CLI at global location.

npm i -g @angular/cli

The command will take some time to install required dependency. Once the command is complete, just execute following command to verify the version

ng version

Now that we have Angular CLI installed, execute following command to generate a Simple Angular Application with default settings.

ng new ng-theme-app

When prompted for routing, select “y” (Not required for theming, but you can test the effect with different modules).
For CSS, choose SCSS as option and then wait for CLI to do it’s job.

Theme Preparation

Before we start with custom themes, it is necessary to do some initial setup. If you open the project in VS Code (or any other editor), you should see styles.scss file at the root/src of the project. Open the file and replace the contents with following

@import '~@angular/material/theming';
@import './app-theme';

@include mat-core();
$primary: mat-palette($mat-indigo);
$accent:  mat-palette($mat-pink, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);
@include material-custom-theme($theme);

Do not worry, if you don’t understand a single line of the above code. The code simply does following

Indicates that we are going to use Angular material theming.
We will define our own theme to customize application specific preferences (app-theme)
Define basic color references required to create a theme (primary and accent).
Include default material theme.
Refer to the custom theme defined in app-theme.

Create a file in src folder as _app-theme.scss and copy following contents

@import '../node_modules/@angular/material/theming';
@import './app/shared/nav-bar/navbar-theme';
@mixin material-custom-theme($theme) {   
$primary: map-get($theme, primary); 
$accent: map-get($theme, accent);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
@include nav-bar-theme($theme);
} 

There is not going much here. We are just referencing few color references that were passed via theme object and include additional theme references present in the project.

Custom Theme

The default theme options look good, we should now proceed with defining alternate themes. I actually copied theme references from Angular Material project.

deeppurple-amber.scss
@import ‘../../app-theme’;
// Define the light theme.
$primary: mat-palette($mat-deep-purple);
$accent: mat-palette($mat-amber, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);
@include material-custom-theme($theme);

indigo-pink.scss
@import ‘../../app-theme’;
// Define the light theme.
$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);
@include material-custom-theme($theme);

pink-grey.scss
@import ‘../../app-theme’;
// Define the dark theme.
$primary: mat-palette($mat-pink);
$accent: mat-palette($mat-blue-grey, A200, A100, A400);
$theme: mat-dark-theme($primary, $accent);
@include angular-material-theme($theme);
@include material-custom-theme($theme);

purple-green.scss
@import ‘../../app-theme’;
// Define the dark theme.
$primary: mat-palette($mat-purple);
$accent: mat-palette($mat-green, A200, A100, A400);
$theme: mat-dark-theme($primary, $accent);
@include angular-material-theme($theme);
@include material-custom-theme($theme);

Each of the above theme changes primary and accent color scheme to add its own flavor of colors. We need to compile each of these theme separately into CSS file. You can make use of node-sass program to compile SCSS. Here is the syntax for same

node-sass src_file destination_file

In our case I compiled the SCSS files using following command

node-sass src\assets\custom-themes\deeppurple-amber.scss src\assets\deeppurple-amber.css

node-sass src\assets\custom-themes\indigo-pink.scss src\assets\indigo-pink.css

node-sass src\assets\custom-themes\pink-grey.scss src\assets\pink-grey.css

node-sass src\assets\custom-themes\purple-green.scss src\assets\purple-green.css

Theme Picker

The most important component of the project – A Theme picker. Looking at the source Material source project, the theme picker is very simple component. It has a button with color picker icon and a menu with color swatches.


<button mat-icon-button [mat-menu-trigger-for]="themeMenu" matTooltip="Select a theme!" tabindex="-1">
<mat-icon>format_color_fill</mat-icon></button>
<mat-menu class="theme-picker-menu" #themeMenu="matMenu" x-position="before">
<mat-grid-list cols="2">
<mat-grid-tile *ngFor="let theme of themes"> 
<div mat-menu-item (click)="installTheme(theme.name)">
<div class="theme-picker-swatch">
<mat-icon class="theme-chosen-icon" *ngIf="currentTheme === theme">check_circle</mat-icon>
<div class="theme-picker-primary" [style.background]="theme.primary">
</div>
</div>
</div>
</mat-grid-tile>
</mat-grid-list>
</mat-menu> 

The ThemePickerComponent.ts file contains logic to switch from one theme to another. First we proceed with collection of colors and reference to CSS files.


themes: CustomTheme[] = [ { primary: '#673AB7',  accent: '#FFC107',  name: 'deeppurple-amber', isDark: false, }, {  primary: '#3F51B5',  accent: '#E91E63',  name: 'indigo-pink',  isDark: false,  isDefault: true, }, { primary: '#E91E63',  accent: '#607D8B',  name: 'pink-grey', isDark: true, }, {  primary: '#9C27B0',  accent: '#4CAF50', name: 'purple-green',  isDark: true, }, ];  

Next we inject the reference of StyleManagerService into constructor. This is a custom service to manipulate DOM nodes and load CSS on demand.

constructor( public styleManager: StyleManagerService, ) { }

Next a function to configure the theme.


installTheme(themeName: string) { 
const theme = this.themes.find(currentTheme=> currentTheme.name === themeName); 
if (!theme) { return; }
if (theme.isDefault) {
this.styleManager.removeStyle('theme');    
} else {
this.styleManager.setStyle('theme', `/assets/${theme.name}.css`);    
}
} 

The function ensures that each time user makes a selection of a theme, it is loaded via StyleManager. In case if a default theme is selected, the dynamically loaded theme has been de-referenced from the HTML.

StyleManager – The stylesheet loader

Under the hood, StyleManager uses the old trick of adding “link” tag to DOM and dynamically loading the stylesheet. Here is the definition of StyleManager


@Injectable({ providedIn: 'root'})
export class StyleManagerService {
constructor() { }
 /** * Set the stylesheet with the specified key. */ 
setStyle(key: string, href: string) { getLinkElementForKey(key).setAttribute('href', href); 
}
 /** * Remove the stylesheet with the specified key. */ 
removeStyle(key: string) { 
const existingLinkElement = getExistingLinkElementByKey(key); 
if (existingLinkElement) {  document.head.removeChild(existingLinkElement); 
} 
}
}
function getLinkElementForKey(key: string) {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string) { 
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
}
function createLinkElementWithKey(key: string) { 
const linkEl = document.createElement('link'); 
linkEl.setAttribute('rel', 'stylesheet'); 
linkEl.setAttribute('type', 'text/css'); linkEl.classList.add(getClassNameForKey(key)); document.head.appendChild(linkEl); 
return linkEl;
}
function getClassNameForKey(key: string) { 
return `style-manager-${key}`;
} 

The trick is pretty simple,
Locate link rel tag with a given custom CSS class name. The class name acts as an Identifier.
If the tag is available change the href attribute to point new CSS file.
If the tag is not found, create it and then add it to document head.

You can now reference <app-theme-picker></app-theme-picker> wherever you want. You can access the entire source code using following Github Repo.

https://github.com/carbonrider/ng-custom-theme

Leave a Comment.