The one thing I didn’t like about Angular 2+....styling overrides
Angular 2+ is great. In its current form it is a powerful set of tools that runs quickly, is easy to use, and provides some fantastic abilities right out of the box. But there is one place I have always found myself frustrated with Angular 2+, and that is in how, contrary to pretty much everything else about it, styles felt painful to override.
The “standard” options have typically consisted of abusing ng-class, something that is much more suited to changing CSS classes at runtime rather than CSS classes that override the defaults of an existing component. Plus then you have to set a condition to use ng-class with, such as an input to the component that ng-class can be used with to set the right set of overrides.
But you can also interpolate the string values, i.e. “ class=”” “. This still requires an input of some sort to figure out what to get, be it an object full of strings passed to the component, or an input to use like ng-class above that the component can pass to a provided service that can then return the appropriate strings to get the object.
Both of these are gross. In both cases we must add yet-another-dependency to our component that it must take regardless of whether or not we are overriding anything, and both are slower than determining classes at compile time, given the classes will change when ng-class runs (leading to possible flash of improperly styled content) or the service returns new strings.
We need a solution that does not require us to provide any more inputs than are necessary, and makes the overriding classes available when the component compiles so that the component compiles with the correct styling.
A solution exists!
We can do this with a single injected dependency, and with configuration instead of code!
I’ll explain first, then walk through the live example.
You can provide objects as values. These can be simple, JSON-like objects, made available with simple “export const …….”. Simply interpolate the strings made available in the object (if no value is provided, it still works, without throwing any errors!) and….you are done. The objects are merely configuration JSON practically speaking, and are provided with a single line of dependency configuration wherever in your component hierarchy you want the overrides to apply.
The only other requirement is that the relevant CSS classes are made available, but by simply making the overriding CSS to available to the component (through the normal array of styles files to draw from) we don’t have to sacrifice usage of the Shadow DOM or its polyfills. In other words, the usage of CSS is exactly as one would expect for a normal Angular component.
This solution does not allow for the possibility of a flash of improperly styled content as Angular renders after dependencies are provided, at which point the CSS classes to be interpolated are already provided. We do not need to provide every override as nonexistent properties will not return anything, and no override is applied.
What it looks like
So, let’s walk through the example, seen here on Stackblitz.
If you check out the my-button.component.ts file you can see that we specify no providers, but instead specify an @Injector for “Values” in the constructor function on line 14. This is all we need to inject the styles override object!
You can also see that we define a list of styles files to draw from on line 6, that includes the normal my-button.styles.css file, as well as two overrides files. This simply gives us access to both the default styles and the overrides. Pretty normal stuff for any Angular component.
In the app.module.ts file we see that we import a “Default” object (on line 23) that imports a default object to provide. This object contains….nothing! We don’t need to override the default, so this can just be an empty object. If we wanted, we could have the default CSS classes defined here instead of the my-button.component.html file, but this is the approach I am using.
I use the same providers line in the components (First overriding component, Second overriding component; see the “providers” property for the components on line 9 of both) that override the styles as in app.module, but using their respective overriding objects (First override, Second override).
And that is it. Nothing else is necessary other than defining CSS rules for your overriding CSS classes that you are overriding with.
In conclusion, we have a fast, configurable option that requires minimum modification to existing code that allows us to override existing styles to normal, vanilla Angular components. No hacky weirdness, no abuse of ill-suited tools, and no conditionals.
Some opinions to be aware of in the example
CSS file organization
I chose to put the CSS classes for overriding in different files to express the fact that they are not part of the normal styling for the component. You can put the CSS class definitions wherever you want.
The Default CSS object
While you could put the default CSS classes to apply in the default object, I prefer to put them in in the HTML. This requires that the CSS used follows a system that is easy to override, as well as the fact that named CSS classes provide fantastic semantic context to HTML. It does require usage of an approach to CSS that emphasizes low specificity values so as to easily override the existing classes - I suggest BEM for this in the next section for this reason. This also makes it easy to use the same default configuration object for all components.
Combined “super” configuration objects
This was a simple example, but in more realistic situations you probably will not want to be handling configuration objects for every configurable component that exists. I still suggest creating a configuration object for each component, but instead of providing them individually, provide a larger “super config” object. This object can be created (and then provided via dependency injection) with a simple function that returns an object that contains all the key-value pairs defined in each object it is given. This can be used exactly like in the example since the keys for the object will exist as expected, and everything else will be ignored.
Don’t overuse it
You probably should not use this excessively. It will most likely work best when there are only 1-3 non-default configurations the component can be in, such as when a certain type of user should have the given element appear in a specific way to them.
Exceptions to this “rule” exist though, such as icons. You could have overrides of this sort defined for every possible icon you want to use a given component for. If those components need a click handler that can easily be provided as a click handling attribute directive by the parent. This is obviously a matter of your particular use case.
Keep CSS easy to override
Another thing is that this works best with a CSS class scheme that encourages a 1-to-1 relationship of CSS classes to HTML elements. In the example I use the BEM approach that I typically favor, but any approach that results in one CSS class for one HTML element will work. This is due to the fact that it is easiest to override an already applied CSS class if you only need….one other CSS class. CSS specificity rules mean this is easiest if there is only a single CSS class that relates to the targeted HTML element, as multiple classes immediately creates complications with regards to specificity.
You can use this approach in React!
Since React components are simply JS objects with added functionality, you can provide the configuration objects via React’s Context API, which provides similar hierarchical dependency injection functionality to what we used here in Angular 2+. Simply append whatever strings are returned by the override to what you provide className.