Merging CSS Classes for use with ngStyle

Lee Winder
Engineering Game Development
5 min readOct 18, 2016

For the release of ng-dynamic-dialog v2, I wanted to move away from defining the dialogs look in TypeScript and use standard CSS classes. This would allow the style of the dialog to be defined alongside the rest of a websites style, and make it compatible with CSS preprocessors like LESS and SASS.

What Was I Aiming For

The main goal was to provide in-built CSS classes which define the default look of the dialogs, and then allow the user to override specific attributes as needed.

They would then be able to specify these CSS classes using Ng2DynamicDialogStyle

// Sets the style of the dialog private 
setDialogStyles() {

// Initialise the style of the dialog
let dialogStyle = new Ng2DynamicDialogStyle();

dialogStyle.background = 'my-custom-background-style';

dialogStyle.dialog = 'my-custom-dialog-style';
dialogStyle.title = 'my-custom-title-style';
dialogStyle.button.general.idle = 'my-custom-button-style';
dialogStyle.button.general.hover = 'my-custom-button-style:hover';

// Set it
this.modalDialog.setStyle(dialogStyle);
}

For example, the in-built style for the background uses the .ng2-dynamic-dialog-background class which defines the position, scale and default colour.

.ng2-dynamic-dialog-background { 

position: fixed;
left: 0;
right: 0;
top: 0;
width: 100%;
height: 100%;
background: #000000;
opacity: 0.4;
}

I wanted the user to be able to define their own class as above to change any element they wanted (for example, changing the colour to green…) without having to duplicate what has already been defined in the stock style

.my-custom-background-style { 
background: #009900;
}

Specifying this in Ng2DynamicDialogStyle should then result in the following background style applied to the background

{ 
position: fixed;
left: 0;
right: 0;
top: 0;
width: 100%;
height: 100%;
background: #009900; <-- Over-ridden colour
opacity: 0.4; }

What Do We Have To Start With

Since I’m passing a style to pre-defined HTML, we have the following options available
* ngClass — allows us to specify a single or set of CSS classes to be applied to the element
* ngStyle — allows us to specify a map of style attributes to be applied to the element

I was initially hopeful that ngClass would actually provide this for me. The documentation _implies_ that the specified classes are applied in order (what else does first, second, third mean if not order?) but that isn’t the case.

All ngClass does is process what is passed, and generate a “class” attribute, meaning it offers nothing above using “class” and as such the ‘order’ is simply lexicographical.

I was hoping maybe Angular 2 could resolve this, but no go.

Since ngStyle allows us to define a map of attributes to be used, I decided this would be the best place to look given the limitations on ngClass.

Accessing and Combining Styles

Finding the Users Style

Note: All code is in TypeScript, but it’s easy enough to follow should it be JavaScript

It’s easy enough to spin through the available styles in a page and find the one the user has specified in Ng2DynamicDialogStyle.

private static getStyleAttributes(styleNameToFind: string) {   // Spin through all the styles in this document 
for (let i = 0; i < document.styleSheets.length; ++i) {
// IE uses rules, rather than cssRules
rulesList = (<any>document.styleSheets[i]).rules ||
(<any>document.styleSheets[i]).cssRules;
for (let x = 0; x < rulesList.length; x++) {

if (rulesList[x].selectorText != null &&
rulesList[x].selectorText === styleNameToFind) {
return rulesList[x].style;
}
}
}
}

But there are a few issues that make this a bit more complicated
* FireFox throws a ‘SecurityError’ if the CSS styles are coming from another domain (other browsers simply return null)
* Angular 2 appends additional naming information — [_ngcontent-ccc-n] — on class names if it’s been defined within a component style sheet (I _think_ this is the only time it appends info, there might be more)

As such, it’s slightly more complicated than the above to find what we need
* We need to catch those ‘SecurityError’ exceptions and carry on
* We need to remove the additional naming information from the style names to allow us to compare

Rather than post the entire code, you can view the complete ‘getStyleAttributes’ function on GitHub
* We append the period onto the style class name if it’s not present (it would be to easy for a user to forget to add that)
* We catch the security exception and ignore that sheet if needed (same if it defaults to null)
* We ignore all Angular 2 added naming data, but also make sure we support style information such as :hover

Combining Styles

Once we have the style, we need to combine them to generate a master style where each provided style builds upon the previous one.

It should be simple

for (let attrname in thisStyle) {   if (thisStyle.hasOwnProperty(attrname)) { 
styleToReturn[attrname] = thisStyle[attrname];
}
}

But this is cross-browser web development, so dream on!
* Firefox doesn’t put the style attribute data in the derived object, so ‘hasOwnProperty’ pretty much skips anything we’re interested in
* Class information contains attribute information as well as style information, which we need to ignore
* If we can’t call ‘hasOwnProperty’, there is base information we need to ignore as well
* Instances of style classes contain values for _every possible_ attribute, which means the above code will constantly over-ride previous attributes even if the user hasn’t specified them

As such we also need to
* Ignore those non-attribute properties
* Ignore the style property indices
* Check a value has actually been provided

Again, the code to do all this is available on GitHub.

Note that we also cache the styles, at the class level rather than instance level, to avoid the constant need to look up the styles every time we need to combine the user styles.

Passing Through Styles to Combine

Once we have the ability to find and combine specific CSS classes, we need to be able to provide them in the order that they should be applied. Since we pass the styles from the user through the Ng2DynamicDialogStyle, we simply merge them when needed.

// Get the list of styles we will use, in the 
// order that we will apply them
let styleList: string[] = [
'ng2-dynamic-dialog-modal-button-close',
];
if (this.dialogStyle.buttonClose.style != null &&
this.dialogStyle.buttonClose.style.length > 0) {
styleList.push(this.dialogStyle.buttonClose.style);
}
// Get the styles we'll use
let styleToUse: any = StyleSheets.mergeStyles(styleList);

For a more complicated example of building these up, you can see the button style generation function on GitHub.

One other thing to note is that in cases we can, we cache the final merged style to avoid having to merge the styles every time they are requested.

What’s Next

This has been tested and verified on Opera, Safari, Firefox and Chrome though unfortunately I personally have not been able to test it on IE (though it’s looking promising).

I’m also expecting some edge cases to pop up in the future, but at the moment it’s running exactly as expected and significantly improves the usability of ng2-dynamic-dialog.

Originally published at engineering-game-dev.com on October 18, 2016.

--

--