Theming Angular Material | Task

Ole Ersoy
May - 08  -  8 min

Scenario

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

Demo