Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Vikander
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Vikander@rvikander )

Alligator.io Code Kata: Defaulting Theme Based On Time-Of-Day In Angular 7.2.11

By Ben Nadel on

I do most of my R&D (Research and Development) in the wee-early hours of the morning. At that time of day, everything is dark; so, I set my computer monitor to the dimmest setting in order to prevent eye strain. In this vein, one of the things that I've come to really appreciate is the fact that Alligator.io defaults their site to a dark theme based on my local time-of-day. Alligator.io has great content; so, it's not uncommon for it to come up in my early-morning Google searches. And, whenever it does, my eyeballs are greeted with a dark-themed digital hug. Feeling inspired by their user-empathy, I wanted to try a small code kata in which I default the theme of my Angular 7.2.11 application based on the user's local time-of-day.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Based on the network payload, I'm certain that Alligator.io uses server-side rendering in order to set the default theme. So, in that regard, my code kata isn't a true representation of their strategy. Since I'm using Angular on GitHub Pages, I only have client-side rendering. As such, my default theme logic will be placed in the HEAD tag of the main page. But, this is just a fun code kata, so I'm not too concerned.

In order to prevent un-themed "FOUC" (Flash of Unstyled Content), we can't isolate the theme logic solely within the Angular application. After all, the Angular application is bootstrapped after the page has loaded and after the remote JavaScript files have been pulled-down over the network. As such, there may be a non-trivial delay between the page-load and the application load.

To account for this valley of inactivity, the default theme logic will be executed by a non-blocking Script tag in the document HEAD. It will work by checking the user's local date/time and then writing a CSS "Theme Class" to the HTML tag. THe rest of the document can then theme itself based on the host-context of this "Theme Class".

I've briefly looked at Theming Angular applications before. Once using the host-context CSS selector; and, once using Custom CSS Properties (aka, CSS Variables). For simplicity's sake, I'm going to use the host-context approach for this code kata.

Thanks to the awesome power of the Date class, checking the user's time-of-day is quite simple. All we have to do is instantiate a Date instance and then inspect the .getHours() method. This will give us the hour within the day, which we can then use to determine the application theme:

  • <!doctype html>
  • <html lang="en">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Alligator.io Code Kata: Defaulting Theme Based On Time-Of-Day In Angular 7.2.11
  • </title>
  •  
  • <script type="text/javascript">
  • (function initializeTheme(){
  •  
  • var hours = ( new Date() ).getHours();
  •  
  • // The default theme is going to be based on the user's local date/time.
  • // Working hours - the so-called "9-to-5" hours - will be considered the
  • // light theme; everything else will be the dark theme.
  • var theme = ( ( 9 <= hours ) && ( hours <= 17 ) )
  • ? "theme--light"
  • : "theme--dark"
  • ;
  •  
  • // NOTE: We're adding the theme to the root element, rather than the body,
  • // because the body isn't available yet in the page processing.
  • document.documentElement.classList.add( theme );
  •  
  • })();
  • </script>
  • <style type="text/css">
  •  
  • html.theme--light body {
  • background-color: #ffffff ;
  • color: #333333 ;
  • }
  •  
  • html.theme--dark body {
  • background-color: #222831 ;
  • color: #f9ffee ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • Alligator.io Code Kata: Defaulting Theme Based On Time-Of-Day In Angular 7.2.11
  • </h1>
  •  
  • <my-app>
  • <!-- App will be injected here, like a boss. -->
  • </my-app>
  •  
  • </body>
  • </html>

As you can see, within the HEAD tag of the document, we quickly look at the Date value and then write a CSS class to the HTML element. If the hours are "working" hours, we use the "light" theme; otherwise, we use the "dark" theme. And, since this happens before the BODY tag even loads, it should render our content in the correct theme before the Angular application even has a chance bootstrapped.

Inside of the Angular application, we want to give the users a chance to override the CSS Theme. But, we also don't want to override the "default" theme on application bootstrap. This means that the Angular application has to consume the DOM (Document Object Model) as the "source of truth" at load time. And, since I'm no longer overly-concerned with a strict DOM abstraction in my Angular applications, I'm just going to inspect the HTML tag from within my ThemeService:

  • // Import the core angular services.
  • import { Injectable } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // These are the CSS classes that are added to the HTML tag to indicate document theme.
  • // One of these will have already been written to the DOM at load time.
  • var LIGHT_THEME_CLASS = "theme--light";
  • var DARK_THEME_CLASS = "theme--dark";
  •  
  • export type Theme = "light" | "dark";
  •  
  • @Injectable({
  • providedIn: "root"
  • })
  • export class ThemeService {
  •  
  • private theme: Theme;
  •  
  • // I initialize the theme service.
  • constructor() {
  •  
  • // By the time this service has been instantiated, the parent page had already
  • // checked the local date/time and has written one of the CSS classes to the
  • // document root. This is the only time we will READ the DOM as the "source of
  • // truth". After the service has been initialized, the service properties will
  • // become the source of truth and the DOM will be updated to reflect the service
  • // state at truth.
  • this.theme = document.documentElement.classList.contains( DARK_THEME_CLASS )
  • ? "dark"
  • : "light"
  • ;
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I get the currently active theme.
  • public getTheme() : Theme {
  •  
  • return( this.theme );
  •  
  • }
  •  
  •  
  • // I set the active theme.
  • public setTheme( theme: Theme ) : void {
  •  
  • this.theme = theme;
  • this.writeThemeToDom();
  •  
  • }
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  • // I synchronize the DOM state with the service state for the theme.
  • private writeThemeToDom() : void {
  •  
  • var classList = document.documentElement.classList;
  •  
  • classList.remove( DARK_THEME_CLASS );
  • classList.remove( LIGHT_THEME_CLASS );
  •  
  • ( this.theme === "dark" )
  • ? classList.add( DARK_THEME_CLASS )
  • : classList.add( LIGHT_THEME_CLASS )
  • ;
  •  
  • }
  •  
  • }

As you can see, when the ThemeService class is instantiated, it looks at the current DOM to see which CSS theme class is active. This is the only time that the ThemeService uses the DOM as the "source of truth" - once the service is initialized, it will become the source of truth and will only ever write its own state back to the DOM.

To test this, I set up a simple App component that allows the user to jump back and forth between the two theme settings:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { Theme } from "./theme.service";
  • import { ThemeService } from "./theme.service";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <p>
  • Select Theme:
  • <a
  • (click)="selectTheme( 'light' )"
  • class="themer"
  • [class.themer--on]="( activeTheme === 'light' )">
  • Light
  • </a>
  • &mdash;
  • <a
  • (click)="selectTheme( 'dark' )"
  • class="themer"
  • [class.themer--on]="( activeTheme === 'dark' )">
  • Dark
  • </a>
  • </p>
  •  
  • <blockquote class="quote">
  • <p class="quote__text">
  • "Victorious warriors win first and then go to war, while defeated
  • warriors go to war first and then seek to win."
  • </p>
  • <footer class="quote__attribution">
  • &mdash; Sun Tzu
  • </footer>
  • </blockquote>
  • `
  • })
  • export class AppComponent {
  •  
  • public activeTheme: Theme;
  •  
  • private themeService: ThemeService;
  •  
  • // I initialize the app component.
  • constructor( themeService: ThemeService ) {
  •  
  • this.themeService = themeService;
  • this.activeTheme = themeService.getTheme();
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I enable the given theme.
  • public selectTheme( theme: Theme ) : void {
  •  
  • this.themeService.setTheme( theme );
  • this.activeTheme = this.themeService.getTheme();
  •  
  • }
  •  
  • }

As you can see, from the App's perspective, it's never dealing with the DOM directly - it only interacts with the ThemeService. The ThemeService then deals with the DOM, writing the appropriate CSS class to the HTML tag. Now, if we load this Angular application outside of the "working hours", we can see that it defaults to the dark theme:


 
 
 

 
 Alligator.io inspired code kata: defaulting to a dark theme at early morning hours. 
 
 
 

As you can see, since we are loading the Angular application well before "working hours", our HEAD tag logic writes the "theme--dark" to the HTML tag. This "theme-dark" CSS class defines some default styles; and, allows the rest of the Angular application to theme itself based on the host-context bindings. For example, in our App component's LESS CSS file, we change the border-color of the blockquote based on the context theme class:

  • // .... truncated.
  •  
  • .quote {
  • border-left: 5px solid #d0d0d0 ;
  • margin: 20px 0px 20px 10px ;
  • padding: 10px 20px 10px 20px ;
  •  
  • &__text {
  • margin: 0px 0px 15px 0px ;
  • }
  •  
  • &__attribution {
  • font-style: italic ;
  • margin-left: 10px ;
  • }
  •  
  • :host-context( .theme--dark ) & {
  • border-color: #266aab ;
  • }
  • }

As you can see, we're using the :host-context() to override the styling of the App component based on the theme CSS class that is currently on the HTML tag.

Anyway, this was just a fun code kata to get my brain warmed-up and ready for the day! It's also a good reminder that not all of the application logic has to reside inside the walls of the Angular code. We have to remember that the Angular code executes inside of the Browser (at least in my context); and that we don't lose access to that outside world.

Shout-out to Alligator.io for the inspiration!



Reader Comments

Excellent stuff. I absolutely love Angular Material & its theming stuff! This reminds me of Apple MacOSX Mojave's new dark mode. Throughout the day, the background gradually changes!

Now I have written a little struct in CF, that you might find useful. You can choose a theme & then pass it into your SPA:

This uses Angular Material Official Theme Primary Colours:

request.materialThemeData = [
    {
      themeName:'theme-1',
      colorName:'$mat-blue-grey',
      primaryIndex:'500',
      primaryHex:'##607D8B'
    },
    {
      themeName:'theme-2',
      colorName:'$mat-red',
      primaryIndex:'500',
      primaryHex:'##F44336'
    },
    {
      themeName:'theme-3',
      colorName:'$mat-pink',
      primaryIndex:'500',
      primaryHex:'##E91E63'
    },
    {
      themeName:'theme-4',
      colorName:'$mat-purple',
      primaryIndex:'500',
      primaryHex:'##9C27B0'
    },
    {
      themeName:'theme-5',
      colorName:'$mat-deep-purple',
      primaryIndex:'500',
      primaryHex:'##673AB7'
    },
    {
      themeName:'theme-6',
      colorName:'$mat-indigo',
      primaryIndex:'500',
      primaryHex:'##3F51B5'
    },
    {
      themeName:'theme-7',
      colorName:'$mat-blue',
      primaryIndex:'500',
      primaryHex:'##3F51B5'
    },
    {
      themeName:'theme-8',
      colorName:'$mat-light-blue',
      primaryIndex:'500',
      primaryHex:'##03A9F4'
    },
    {
      themeName:'theme-9',
      colorName:'$mat-cyan',
      primaryIndex:'500',
      primaryHex:'##00BCD4'
    },
    {
      themeName:'theme-10',
      colorName:'$mat-teal',
      primaryIndex:'500',
      primaryHex:'##009688'
    },
    {
      themeName:'theme-11',
      colorName:'$mat-green',
      primaryIndex:'500',
      primaryHex:'##4CAF50'
    },
    {
      themeName:'theme-12',
      colorName:'$mat-light-green',
      primaryIndex:'500',
      primaryHex:'##8BC34A'
    },
    {
      themeName:'theme-13',
      colorName:'$mat-lime',
      primaryIndex:'500',
      primaryHex:'##CDDC39'
    },
    {
      themeName:'theme-14',
      colorName:'$mat-yellow',
      primaryIndex:'500',
      primaryHex:'##FFEB3B'
    },
    {
      themeName:'theme-15',
      colorName:'$mat-amber',
      primaryIndex:'500',
      primaryHex:'##FFC107'
    },
    {
      themeName:'theme-16',
      colorName:'$mat-orange',
      primaryIndex:'500',
      primaryHex:'##FF9800'
    },
    {
      themeName:'theme-17',
      colorName:'$mat-deep-orange',
      primaryIndex:'500',
      primaryHex:'##FF5722'
    },
    {
      themeName:'theme-18',
      colorName:'$mat-brown',
      primaryIndex:'500',
      primaryHex:'##795548'
    },
    {
      themeName:'theme-19',
      colorName:'$mat-gray',
      primaryIndex:'500',
      primaryHex:'##9E9E9E'
    }
  ];
Reply to this Comment

And here is a complimentary '.scss' file template that uses the 'themeName' to determine the website's colour theme. Actually, I will send it to you by Twitter DM...

Reply to this Comment

Actually, Twitter does not allow me to send files, only images.

If you are on Slack, I can send it to you.

It essentially allows you to choose a colour theme and then it plugs into the '.scss' file and voila, your website colour is sorted. There is a section for custom selectors that you will probably want to add, but there are a whole host of Angular Material Colour functions that you can use to slightly alter the hue & opacity.

I built it, based on research I carried out on this SO question:

https://stackoverflow.com/questions/45089178/how-to-get-primary-or-accent-color-of-currently-applied-theme-in-angular-materia

Links that are really useful are:

https://medium.com/@tomastrajan/the-complete-guide-to-angular-material-themes-4d165a9d24d1

https://material.angular.io/guide/theming-your-components

Reply to this Comment

@Charles,

Material Design is one of those things that been on my list of "things to investigate" for the longest time; but, I just never seem to be able to carve-out time to look at it :D Which is a shame, because I suspect that it has a lot of really interesting architectural patterns to learn from. Especially around things like Theming and component interactions.

One of these days!!! :D

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.