Search code examples
angularsassangular-materialthemingangular-lazyloading

Trying to lazy load custom themes dynamically based on angular material


I'm writing an angular 12 website with angular material. This project supports several domains and on different domains it supposed to show different theme.

so what I'm trying to resolve here is to be able to load themes dynamically with lazy loading.

my custom theme works.. basic angular material functionality doesn't, and I do get the message

Could not find Angular Material core theme. Most Material components may not work as expected. For more info refer to the theming guide: https://material.angular.io/guide/theming

I googled a lot and if I load the css file properly and append it to the head, it should work properly.

ok.. so first..

I created app/themes directory

I have 3 themes, windy.scss, windy-car.scss, and windy-shop.scss, each one of them requires common.scss.

i have several components that uses variables that I created in each file with specific color, and the common.scss actually decorate the components when the chosen colors.

for example:

common.js includes:

app-root {

  div#my-mat-drawer {
    background: $container-bg;
  }

}

and windy.scss contains:

$container-bg:  #aabbcc;

i'll show one scss file as an example, when i used just one theme it worked so i know the format is good.

this is windy.scss:

@use '../../node_modules/@angular/material/index' as mat;
$container-bg:  #aabbcc;
$container-color:  white;

$button1-color: #ccdd99;
$button2-color: #112233;

$product-card-text: red;

$category-title-bg: blue;

$product-categories-bg: yellow;

$toolbar-bg-color: green;

$dark-primary-text: rgba(black, 0.87);
$dark-secondary-text: rgba(black, 0.54);
$dark-disabled-text: rgba(black, 0.38);
$dark-dividers: rgba(black, 0.12);
$dark-focused: rgba(black, 0.12);
$light-primary-text: white;
$light-secondary-text: rgba(white, 0.7);
$light-disabled-text: rgba(white, 0.5);
$light-dividers: rgba(white, 0.12);
$light-focused: rgba(white, 0.12);

$my-cyan-palette: (
  50: #e0f7fa,
  100: #b2ebf2,
  200: #80deea,
  300: #4dd0e1,
  400: #26c6da,
  500: #00bcd4,
  600: #00acc1,
  700: #0097a7,
  800: #00838f,
  900: $button2-color,
  A100: #84ffff,
  A200: #18ffff,
  A400: #00e5ff,
  A700: #00b8d4,
  contrast: (
    50: $dark-primary-text,
    100: $dark-primary-text,
    200: $dark-primary-text,
    300: $dark-primary-text,
    400: $dark-primary-text,
    500: $light-primary-text,
    600: $light-primary-text,
    700: $light-primary-text,
    800: $light-primary-text,
    900: $light-primary-text,
    A100: $dark-primary-text,
    A200: $dark-primary-text,
    A400: $dark-primary-text,
    A700: $dark-primary-text,
  )
);

$my-lime-palette: (
  50: #f9fbe7,
  100: #f0f4c3,
  200: #e6ee9c,
  300: #dce775,
  400: #d4e157,
  500: #cddc39,
  600: #c0ca33,
  700: #afb42b,
  800: #9e9d24,
  900: $button1-color,
  A100: #f4ff81,
  A200: #eeff41,
  A400: #c6ff00,
  A700: #aeea00,
  contrast: (
    50: $dark-primary-text,
    100: $dark-primary-text,
    200: $dark-primary-text,
    300: $dark-primary-text,
    400: $dark-primary-text,
    500: $dark-primary-text,
    600: $dark-primary-text,
    700: $dark-primary-text,
    800: $dark-primary-text,
    900: $light-primary-text,
    A100: $dark-primary-text,
    A200: $dark-primary-text,
    A400: $dark-primary-text,
    A700: $dark-primary-text,
  )
);



$windy-store-primary: mat.define-palette($my-cyan-palette, 900);
$windy-store-accent: mat.define-palette($my-lime-palette,900); 
$windy-store-warn: mat.define-palette($my-cyan-palette,900);

 $windy-store-theme: mat.define-light-theme((
  color: (
    primary: $windy-store-primary,
    accent: $windy-store-accent,
    warn: $windy-store-warn,
  )
));

@include mat.all-component-themes($windy-store-theme);
@import "common";

now.. in angular.json i added these files with inject: false:

"styles": [
              "src/styles.scss",
              {
                "input": "src/themes/windy.scss",
                "bundleName": "windy",
                "inject": false
              },
              {
                "input": "src/themes/windy-car.scss",
                "bundleName": "windy-car",
                "inject": false
              },
              {
                "input": "src/themes/windy-shop.scss",
                "bundleName": "windy-shop",
                "inject": false
              }
            ]

I created a style-manager service and a theme.service and added them to the provider in app.module.ts.

style-manager-service.ts

/**
 * Copied from https://github.com/angular/material.angular.io/blob/master/src/app/shared/style-manager/style-manager.ts
 */

import { Injectable } from "@angular/core";

@Injectable()
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 `app-${key}`;
}

and theme.service.ts

import { Injectable } from "@angular/core";

import { StyleManagerService } from "./style-manager.service";

@Injectable()
export class ThemeService {
  constructor(
    private styleManager: StyleManagerService
  ) {}


  setTheme(themeToSet: string) {
    this.styleManager.setStyle(
      "theme",
      `${themeToSet}.css`
    );
  }
}

and I injected private readonly themeService: ThemeService in app.component.ts and ran this.themeService.setTheme('windy-car'); in the constructor.

and the results.. i do see the custom colors for the chosen theme, but angular material functionality is broken. color=primary doesn't work and other related styles to angular material do not show.

for example in the toolbar i have color="primary" and still the color is white.

I'm sure it's related to the error Could not find Angular Material core theme. Most Material components may not work as expected. For more info refer to the theming guide: https://material.angular.io/guide/theming but I have no idea how to resolve this.

any ideas ?

** update 1 **

when i added the following in styles.scss:

@import '~@angular/material/theming';

@include mat-core();

i still get the error

Could not find Angular Material core theme. Most Material components may not work as expected. For more info refer to the theming guide: https://material.angular.io/guide/theming

angular material components work properly but it ignores my chosen colors completely.


Solution

  • i was able to finally resolve it :)

    so in the main style.scss file I added:

    @use '~@angular/material' as mat;
    @import '~@angular/material/theming';
    @include mat.core();
    

    I also added a default theme that i override with my files just not to have the error "could not find angular material core theme"

    $a-primary: mat.define-palette(mat.$indigo-palette);
    $a-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
    
    // The warn palette is optional (defaults to red).
    $a-warn: mat.define-palette(mat.$red-palette);
    
    // Create the theme object. A theme consists of configurations for individual
    // theming systems such as "color" or "typography".
    $a-theme: mat.define-light-theme((
      color: (
        primary: $a-primary,
        accent: $a-accent,
        warn: $a-warn,
      )
    ));
    
    // Include theme styles for core and each component used in your app.
    // Alternatively, you can import and @include the theme mixins for each component
    // that you are using.
    @include mat.all-component-themes($a-theme);
    

    • update - I actually removed that. i see in a split second this theme that i defined and then it changes to my lazy loaded theme. i could change just this theme to write if i want so the switch won't be as bad.. but I removed it completely, besides having this warning it doesn't affect my app in any way.

    and in my individual theme style file I just needed @use '../../node_modules/@angular/material/index' as mat;

    now in common scss instead of directly pointing to my components, i created a class and added that class name to the component.

    so for example:

    instead of:

    app-toolbar {
      mat-toolbar {
        background: $toolbar-bg-color;
      }
    }
    

    well in the original i had many ::ng-deep but i removed them all because all the components that i wanted to modify has panelClass property and my components.. i just gave them a class name instead.

    so i just did:

    .app-toolbar-mat-toolbar {
        background: $toolbar-bg-color;
      }