We will be using the following markup to demo Angular Material theme creation and switching. This is a link to the Youtube.
<body [class]="s.OS.S.theme.obs | async">
<h2>Theme Demo</h2>
<mat-form-field appearance="fill">
<mat-label>Theme Selection</mat-label>
<mat-select [formControl]="themeSelect">
<mat-option *ngFor="let theme of themes" [value]="theme">{{theme}}</mat-option>
</mat-select>
</mat-form-field>
<section>
<div class="example-label">Basic</div>
<div class="example-button-row">
<button mat-button>Basic</button>
<button mat-button color="primary">Primary</button>
<button mat-button color="accent">Accent</button>
<button mat-button color="warn">Warn</button>
<button mat-button disabled>Disabled</button>
<a mat-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="example-label">Raised</div>
<div class="example-button-row">
<button mat-raised-button>Basic</button>
<button mat-raised-button color="primary">Primary</button>
<button mat-raised-button color="accent">Accent</button>
<button mat-raised-button color="warn">Warn</button>
<button mat-raised-button disabled>Disabled</button>
<a mat-raised-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
</body>
The demo is available on Stackblitz.
We have placed the end result in this Github Repository in case you wish to clone it and run the code.
git clone git@github.com:fireflysemantics/angular-theming-example.git
cd angular-theming-example
npm i
ng serve -o
Approach
Setup
First create the project angular-theming-example using the CLI.
ng new angular-theming-example --routing --style=scss skipTests=true
cd angular-theming-example
Add Angular Material.
ng add @angular/material
Install our dependencies. We will be using a Firefly Semantics Slice Object Store to house the theme state needed to switch themes.
npm i @fireflysemantics/slice @types/nanoid nanoid
Theme Initialization
Within the src
directory create the file themes.scss
containing our themes.
The primary
color will be mat-indigo
with the default hue (color) set to 500
and the lighter and darker hues (colors) set to 200
and 800
correspondingly.
If you would like to know more about the material design color palettes and how they are structured see Understanding the Angular Material Color Palette and YouTube.
The accent color is set to mat-cyan
and Angular material will apply the default weights (500, 100, 700) for the default
, lighter
, and darker
hues since we don’t specify these.
The warn
color will be set to mat-deep-orange
with a default weight of A200
.
$my-theme-primary: mat-palette($mat-indigo, 500, 200, 800);
$my-theme-accent: mat-palette($mat-cyan);
$my-theme-warn: mat-palette($mat-deep-orange, A200);
$light-theme: mat-light-theme(
$my-theme-primary,
$my-theme-accent,
$my-theme-warn
);
// ====================================
// Make sure to have a dark background
// Dropdowns will blend in with light
// backgrounds.
// ====================================
$dark-theme: mat-dark-theme(
$my-theme-primary,
$my-theme-accent,
$my-theme-warn
);
The dark
and light
themes are defined with the mat-light-theme
and mat-dark-theme
Sass functions.
After this we need to update styles.scss
to include our themes.
@import '~@angular/material/theming';
// =====================================
// always include only once per project
// =====================================
@include mat-core();
// =====================================
// import our custom themes
// =====================================
@import 'themes.scss';
// =====================================
// Define the light theme
// =====================================
.light-theme {
@include angular-material-theme($light-theme);
}
.dark-theme {
// use our theme with angular-material-theme mixin
@include angular-material-theme($dark-theme);
background-color: black;
}
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
Note that we have defined the light-theme
and dark-theme
classes.
Theme State Service
In order to switch themes we will be emitting the currently select theme class
via an Observable<string>
. We will use a Firefly Semantics Slice Object Store to initialize and manage this state. The service is located in src/services/state.service.ts
.
import { Injectable } from "@angular/core";
import { KeyObsValueReset, ObsValueReset, OStore, OStoreStart } from '@fireflysemantics/slice'
import { Observable } from "rxjs";
const START: OStoreStart = {
theme: { value: 'dark-theme' }
}
interface ISTART extends KeyObsValueReset {
theme: ObsValueReset
}
@Injectable({
providedIn: "root"
})
export class StateService {
constructor() { }
public OS: OStore<ISTART> = new OStore(START)
public selectedTheme$:Observable<string> = this.OS.S.theme.obs
}
For more details on how this works see the Youtube Firefly Semantics Slice Object Store Introduction.
Or the Introduction to the Firefly Semantics Slice Reactive Object Store.
Application Theme Switching
Within app.component.ts
we will initialize the current theme select control with the current theme from our state service and subscribe
to the select control valueChanges
observable such that we can switch our theme.
import { OverlayContainer } from '@angular/cdk/overlay';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { StateService } from './services/state.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
//========================================
// The theme selection control
// initialized with a snapshot
// of the initial theme value
// from the Object Store.
//========================================
public themeSelect = new FormControl(this.s.OS.snapshot(this.s.OS.S.theme));
//========================================
// Array to populate the mat-select
// for theme selection
//========================================
themes: string[] = ['Light', 'Dark'];
//========================================
// Map Light to light-theme and
// Dark to dark-theme
//========================================
themeMap: Map<string, string> = new Map()
constructor(
public s: StateService,
private overlayContainer: OverlayContainer
) {
//========================================
// Map Light to light-theme and
// Dark to dark-theme
//========================================
this.themeMap.set('Light', 'light-theme')
this.themeMap.set('Dark', 'dark-theme')
}
ngOnInit(): void {
//========================================
// Subscribe to the themeSelect control.
// When the user selects a new theme
// the control emmits the theme.
// We put the new theme value in the object
// store, and the object store theme
// observable is then used by the app.component.html
// template to switch the theme.
//========================================
this.themeSelect.valueChanges.subscribe(themeColor => {
const theme:string | undefined = this.themeMap.get(themeColor)
this.s.OS.put(this.s.OS.S.theme, theme)
this.removeThemeClasses()
this.addThemeClass()
})
//========================================
// Remove any current theme classes from
// overlay container. Then add the default
// theme class.
//========================================
this.removeThemeClasses()
this.addThemeClass()
}
/**
* Remove css classes that contain the classPostfix
* string from the CDK overlayContainer.
*/
removeThemeClasses(classPostfix: string = '-theme') {
const overlayContainerClasses = this.overlayContainer.getContainerElement().classList;
const themeClassesToRemove = Array.from(overlayContainerClasses).filter((item: string) => item.includes(classPostfix));
if (themeClassesToRemove.length) {
overlayContainerClasses.remove(...themeClassesToRemove);
}
}
/**
* Add the themeClass to the overlay container class list.
* The current theme class is retrieved from the state service
* Object Store.
*/
addThemeClass() {
const themeClass:string = this.s.OS.snapshot(this.s.OS.S.theme)
this.overlayContainer.getContainerElement().classList.add(themeClass)
}
}
Note the methods addThemeClass()
and removeThemeClasses()
. These are used to add / remove the current theme class to / from the Angular CDK OverlayContainer
. The container is used in the Snackbar
, MatSelect
and so on components that render overlays.
Finally we have our application template.
<body [class]="s.OS.S.theme.obs | async">
<h2>Theme Demo</h2>
<mat-form-field appearance="fill">
<mat-label>Theme Selection</mat-label>
<mat-select [formControl]="themeSelect">
<mat-option *ngFor="let theme of themes" [value]="theme">{{theme}}</mat-option>
</mat-select>
</mat-form-field>
<section>
<div class="example-label">Basic</div>
<div class="example-button-row">
<button mat-button>Basic</button>
<button mat-button color="primary">Primary</button>
<button mat-button color="accent">Accent</button>
<button mat-button color="warn">Warn</button>
<button mat-button disabled>Disabled</button>
<a mat-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="example-label">Raised</div>
<div class="example-button-row">
<button mat-raised-button>Basic</button>
<button mat-raised-button color="primary">Primary</button>
<button mat-raised-button color="accent">Accent</button>
<button mat-raised-button color="warn">Warn</button>
<button mat-raised-button disabled>Disabled</button>
<a mat-raised-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
</body>
Note that within the body element [class]
property we assign the current theme using our state service.
<body [class]="s.OS.S.theme.obs | async">
And that’s it. To check out the end result clone the github repository and run it.
git clone git@github.com:fireflysemantics/angular-theming-example.git
cd angular-theming-example
npm i
ng serve -o