5
votes

I have an Angular 4 app using Material 2 (current version, 2.0.0-beta.11).

I have followed the documentation for theming and created two themes, a light theme and a dark theme. Since I do not use a md-sidenav component, I have put the mat-app-background class on the body element, which applies the current theme's background color (a light grey for a light theme, and a dark grey for a dark theme.)

The problem is that when my theme switcher is toggled (a simple button that adds or removes a class, nothing fancy, like this answer), the background color from the mat-app-background is not updated.

I am unable to create a plunker that successfully uses custom themes, but here's the basic gist:

index.html

<body class="mat-app-background">
  <app-root></app-root>
</body>

app.component.html

<main [class.dark-theme]="isDarkTheme">
  <button md-raised-button color="primary" (click)="toggleTheme()">Toggle</button>
  <!-- other content -->
</main>

app.component.ts

isDarkTheme: boolean = false;
toggleTheme() { this.isDarkTheme = !this.isDarkTheme; }

theme.scss

@import '~@angular/material/theming';
@include mat-core();

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

// alternate dark theme
$dark-primary: mat-palette($mat-blue-grey);
$dark-accent:  mat-palette($mat-amber, A200, A100, A400);
$dark-warn:    mat-palette($mat-deep-orange);
$dark-theme:   mat-dark-theme($dark-primary, $dark-accent, $dark-warn);

.dark-theme {
  @include angular-material-theme($dark-theme);
}

When I click the button, it successfully toggles the theme: the class is applied, the button changes color from the light theme primary color to the dark theme primary color. But the background color remains the light theme's background color.

The default background color "sticks" and is unaffected by the theme switch. What am I missing? Is there a way for the background color to be automatically updated without a mixin/manual redefinition?

(Note: clean angular cli project, using the themes copy pasted directly from the angular material documentation. Angular 4.4.4, material 2.0.0-beta.11.)

2

2 Answers

7
votes

I found the answer indirectly acknowledged in a closed issue on the angular material 2 github project, so I'll outline below what is going on and how I applied a workaround to handle this "feature."

Angular Material 2 theming only applies to Material components.

That is to say, if you have your entire app nested inside of a md-sidenav (or mad-sidenav) component, you're fine since that's a Material component. But if you're not using that and instead go the route of applying mat-app-background as indicated in the documentation, you only get the initial-applied theme's styling.

In order to get this working for non-material-component elements (eg. regular html elements), you need to apply a scss mixin in order to apply these styles. Furthermore, you'll need to style all of your non-material-component elements manually.

To begin, I updated my theme.scss to include a mixin that targeted the main element. As in my initial post, the main wraps the entire app.component and is the class where I apply the dark-theme class.

@mixin html-theme($theme) {
  & {
    $background: map-get($theme, background);
    $foreground: map-get($theme, foreground);

    background-color: mat-color($background, background);
    color: mat-color($foreground, text);

    // other html element styling here
  }
}

Here I'm manually applying the theme's background color and foreground color as the background and font color, respectively, on the main element. It's important to note that I've tweaked the general styling of my main element to be the full height of the browser window.

To apply this, I @include the mixin within the themed class definition in the sass file. Importantly, I also had to assign my default theme to its own class; otherwise the default would always trigger the mixin and overwrite the active theme's styling with the default's.

// light (default) theme
.light-theme {
  $lt-primary: mat-palette($mat-indigo);
  $lt-accent:  mat-palette($mat-pink, A200, A100, A400);
  $lt-theme:   mat-light-theme($lt-primary, $lt-accent);

  @include angular-material-theme($lt-theme);

  @at-root main.light-theme {
    @include html-theme($lt-theme);
  }
}

// alternate dark theme
.dark-theme {
  $dark-primary: mat-palette($mat-blue-grey);
  $dark-accent:  mat-palette($mat-amber, A200, A100, A400);
  $dark-warn:    mat-palette($mat-deep-orange);
  $dark-theme:   mat-dark-theme($dark-primary, $dark-accent, $dark-warn);

  @include angular-material-theme($dark-theme);

  @at-root main.dark-theme {
    @include html-theme($dark-theme);
  }
}

The reason I used the sass @at-root directive to re-target only the main element explicitly is because the themed class also needs to be applied to the CDK overlay in order to apply to elements like drop downs and the date picker. (Obnoxious, but at this point in the beta you have to do it manually.)

To support the two different theme classes, I adjusted the app.component.html trivially to toggle between 'light-theme' and 'dark-theme':

<main [class.dark-theme]="isDarkTheme" [class.light-theme]="!isDarkTheme">

And, as mentioned, we need to also re-theme the CDK overlay, so I updated to the app.component.ts file to add the appropriate theme class to that as well:

isDarkTheme = false;

constructor(private overlayContainer: OverlayContainer) {
  this.setOverlayClass();
}

toggleTheme() {
  this.isDarkTheme = !this.isDarkTheme;
  this.setOverlayClass();
}

private setOverlayClass() {
  this.overlayContainer.themeClass = (this.isDarkTheme ? 'dark-theme' : 'light-theme');
}

(Note, theme-class was removed when the cdk project was merged into the angular material project. For newer projects, you need to manipulate the DOMTokenList directly, eg. overlayContainer.getContainerElement().classList.toggle('light-theme').)

With all of these changes applied, toggling between light and dark themes now works.

It might just be easier to nest your app in a sidenav and just not use the sidenav part.

0
votes

Very late to this party, but if your Angular app isn't nested within a parent Material component, it will not take on the theme properties you have set. As @RoddyoftheFrozenPass mentioned, this could be solved by encapsulating your app within an material component like a side-nav, but it's easier to just add the mat-app-background class to your body element within index.html.

https://material.angular.io/guide/theming