Sunday, January 23, 2022

Manage Theme Colors in Angular Using an Object Map

In the Using an Angular Service to Read Sass Variables tutorial we learned how to fetch theme colors that are defined in a Sass .scss file using a service. The associated demo app provided radio buttons to select between standard theme colors and a lighter or darker variation. Here is an example of how that looked.

How to read Sass variables in Angular

 

While the previous article described how the app loaded the colors from the Sass .scss file, this one will cover how to store color and variation combinations for easy access from the ColorService.

Mapping Loaded Color Properties to a Specific Theme

Recall that the ColorService’s loadColors() method simply loaded each theme color into a Map as a CSS name/value pair:

ColorMap example in CSS and Angular

 

From there, we could just store each of these to a variable such as backgroundColor, borderColor, etc. The problem with that is that the number of variables required grows exponentially with each variation required. For example, if we had only a light and dark option, we would now need three variables for each property – one for the property and one for each state. A better idea might be to simply update the Map keys to accept a property+option combination. Having gone through the process of reading in the CSS properties in the last article, let’s refactor the loadColors() code to transform CSS properties into a more useful key format.

Read: Introduction to TypeScript and Its Features

Iterating over Enums with TypeScript

Enums are not supported by vanilla JavaScript, so, behind the scenes, TypeScript enums are transpiled into associative arrays:

//in TypeScript
export enum PropertyNames {
  background  = 'background',
  hover       = 'hover',
  focusborder = 'focus-border'
}

export enum ColorOptions {
  standard = '',
  light    = 'light',
  dark     = 'dark'
}

//transpiled JavaScript
var PropertyNames;
(function (PropertyNames) {
    PropertyNames["background"] = "background";
    PropertyNames["hover"] = "hover";
    PropertyNames["focusborder"] = "focus-border";
})(PropertyNames = exports.PropertyNames || (exports.PropertyNames = {}));

var ColorOptions;
(function (ColorOptions) {
    ColorOptions["standard"] = "";
    ColorOptions["light"] = "light";
    ColorOptions["dark"] = "dark";
})(ColorOptions = exports.ColorOptions || (exports.ColorOptions = {}));

As such, we can obtain an enums keys and values using the ES2017 Object.keys() and Object.values() methodsn order to use them; we’ll need to configure the TypeScript library to ES2017 or later by setting the –lib compiler option. For example, using tsconfig.json:

"compilerOptions": {
    "lib": ["es2017", "dom"]
}

Now we can iterate over the PropertyNames and associate ColorOptions with each:

@Injectable()
export class ColorService {
  private _colorMap: Map<string, string> = new Map();
  
public loadColors() {
  // Read the custom property of body section with given name:
  const appElement = 
    document.getElementsByClassName('color-demo-app');
  if (appElement && appElement.length > 0) {
    const appStyles = window.getComputedStyle(appElement[0]);
    Object.values(PropertyNames).forEach(propertyName => {
      Object.values(ColorOptions).forEach(colorOption => {
        //...
      });
    });
  }
}

Inside the nested forEach() loops, we can assemble the CSS variable name from a combination of constants and enum values. It produces a potential CSS variable name like “–dark-hover-color” or “–focus-border-color”:

Object.values(PropertyNames).forEach(propertyName => {
  Object.values(ColorOptions).forEach(colorOption => {
    const cssVariableName = CSS_PREFIX
      + (colorOption 
        ? `${colorOption}${CSS_DELIMITER}` 
        : '')
      + `${propertyName}${CSS_DELIMITER}`
      + CSS_SUFFIX;
  });
});

I said “potential” because we have not actually defined every possible color option for each property name. Here are all of the theme colors contained in the app.component.css file:

.color-demo-app {
  --background-color: #{$backgroundColor};
  --light-background-color: #{$lightBackgroundCcolor};
  --hover-color: #{$hoverColor};
  --dark-hover-color: #{$darkHoverColor};
  --focus-border-color: #{$focusBorderColor};
  --dark-focus-border-color: #{$darkFocusBorderColor};
}

Hence, the CSSStyleDeclaration’s getPropertyValue() method could come up empty for some CSS variable names, for example, “–dark-background-color”. In such a case, the cssVariableValue below would contain an empty (”) string, which we can check for using a JavaScript falsiness test:

interface ColorProperty {
  name: PropertyNames,
  option: ColorOptions
} 

//inside the class
const cssVariableValue = 
  appStyles.getPropertyValue(cssVariableName)
           .replace(' ', '');
if (cssVariableValue) {
  const colorMapKey = <ColorProperty>{
    name: propertyName,
    option: colorOption
  };
  this._colorMap.set(
    JSON.stringify(colorMapKey),
    cssVariableValue
  );
}

Read: Choosing Between TypeScript String Literals and Enums

Using Objects as Map Keys

We’d like to store obtained CSS variable values in the _colorMap using an object key that combines the property name and color option. Problem is, objects do not always work as Map keys! This is due to the === equality test behaving differently for objects than simple types such as strings and numbers. In the case of objects, === only evaluates to true if both objects are one and the same! Since we recreate the key every time we retrieve a color value, an object key won’t work. In such instances, a popular workaround is to marshal the object into string form using JSON.stringify(). It takes an object such as this:

{
 name: "hover",
 option: "light"
}

and transforms it into this:

"{\"name\":\"hover\",\"option\":\"\"}"

Now we can add an entry to the _colorMap as follows:

const colorMapKey = <ColorProperty>{
  name: propertyName,
  option: colorOption
};
this._colorMap.set(
  JSON.stringify(colorMapKey),
  cssVariableValue
);

Setting Theme Defaults

After loading all of the defined colors and their associated options, the _colorMap now contains six entries:

"{\"name\":\"background\",\"option\":\"\"}" : "#52acf0"

"{\"name\":\"background\",\"option\":\"light\"}" : "#9dd0f7"

"{\"name\":\"hover\",\"option\":\"\"}" : "blue"

"{\"name\":\"hover\",\"option\":\"dark\"}" : "#000099"

"{\"name\":\"focus-border\",\"option\":\"\"}" : "darkgray"

"{\"name\":\"focus-border\",\"option\":\"dark\"}" : "#434343"

These represent all possible color+option combinations. All that’s left to do is define the theme colors that we are actually using. To do that, we’ll introduce a new _themeMap that contains only the property name as a key, along with the active color. In the setThemeDefaults() method (called at the end of loadColors()), we’ll start with the standard colors:

private _themeMap: Map<string, string> = new Map();

public get themeMap() {
  return this._themeMap;
}
  
private setThemeDefaults() { 
  Object.values(PropertyNames).forEach(propertyName => {
    const color = 
      this._colorMap.get(JSON.stringify(<ColorProperty>{
        name: propertyName,
        option: ColorOptions.standard
      }));
    this._themeMap.set(propertyName, color);
  });
}

That gives us the following theme colors which we see when the app first loads:

background : "#52acf0"

hover : "blue"

"focus-border" : "darkgray"

Applying Color Options in Angular

Radio buttons’ change event is bound to the setColor() method of the AppComponent. It sets the property name and color option combination and forwards the request to the color service method of the same name:

public setColor(
  name: PropertyNames, 
  option: ColorOptions = ColorOptions.standard
) {
  this.colorService.setColor({
    name: name,
    option: option
  })
}

The service method fetches the entry from the _colorMap and updates the _themeMap with the new color:

public setColor(colorProperty: ColorProperty) {
  const colorProps = JSON.stringify(colorProperty);
  const themeColor = this._colorMap.get(colorProps);
  this._themeMap.set(colorProperty.name, themeColor);
}

There’s a demo with today’s code on stackblitz.

Conclusion to Managing Theme Colors in Angular

In today’s article we learned how to manage theme colors using a combination of “Object” Map and string (enum) Map. The first Map stored all available colors and variations, while the second kept track of current theme colors. I put all of the color and option definitions in the ColorService, but you may choose to house them in the AppComponent and pass them to the service if you want it to be more agnostic. Your application may even fetch color definitions from another source. The permutations are indeed endless.

Read: Ten Ways to Use Angular JS

Robert Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Popular Articles

Featured