Sanity Check: Shared Style Urls Are Only Compiled Into Angular 5.0.1 Once
CAUTION: This is primarily a "note to self" post that may have drastically changed the way I think about organizing the CSS in my Angular apps.
CAVEAT: This has been explored using the JIT (Just In Time) compiler (with Webpack and the angular2-template-loader). But, I assume that the AOT (Ahead Of Time) compiler works the same way.
One of the features that I love the most in Angular is the concept of simulated view encapsulation. This feature allows CSS to be scoped to a single component, preventing the CSS rules in one component from bleeding into another component. This works great when a block of CSS is "owned" by a single component. But, it makes it unclear as to where one should place global CSS styles; and, how to interact with the increased selector specificity of locally-scoped CSS rules.
The more that I think about this problem, the more I wonder if the actual point-of-friction is the mixing of two different methodologies? Instead of trying to overlay the "one giant CSS file" mentality on top of the encapsulated view approach to Angular components, perhaps I need to think about bringing more of the CSS - including the shared CSS - into the individual component definitions.
Fortunately - and I can only assume it is for this exact purpose - Angular component meta-data allows you to associate multiple "styleUrls" with a single template. This means that any given component can be provided with a local CSS file as well as zero-or-more shared CSS files. In the past, I've shied away from this approach, partly because I was still thinking about structure my CSS in large files; but, also partly because I was afraid that this would lead to a lot of duplicated CSS in the compiled assets.
The other day, however, it suddenly occurred to me that this assumption - the duplication of CSS - wasn't actually based on any evidence. And, what's more, the more I become familiar with Webpack, the less this assumption makes any sense to begin with. As such, I felt it was time to actually see what happens when I share a CSS file across two different Angular components.
To test this, I created two simple components, WidgetOneComponent and WidgetTwoComponent, that share a "widget-shared.less" file on top of their own local "widget-one.component.less" and "widget-two.component.less" files, respectively. Here is the shared LESS CSS file:
/***** SHARED WIDGET LESS FILE. *****/
:host {
border: 1px solid #CCCCCC ;
border-radius: 3px 3px 3px 3px ;
display: block ;
margin: 16px 0px 16px 0px ;
padding: 17px 17px 17px 17px ;
}
*:first-child {
margin-top: 0px ;
}
*:last-child {
margin-bottom: 0px ;
}
strong {
text-decoration: underline ;
text-transform: uppercase ;
}
And, here is WidgetOneComponent:
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "widget-one",
styleUrls: [
"./widget-shared.less", // <---- This is the SHARED LESS file. ----
"./widget-one.component.less"
],
template:
`
Hello, I am <strong>Widget One</strong>. Some of my styles are shared.
`
})
export class WidgetOneComponent {
// ...
}
Notice that in the component meta-data, I'm providing two different URLs in the "styleUrls" property. The first is the shared CSS file; the, second is the local CSS file. The local CSS file does nothing but override some of the shared properties:
:host {
border-color: darkcyan ;
}
strong {
color: darkcyan ;
}
The second widget is essentially the same exact code, but with the term "one" replaced with the term, "two":
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "widget-two",
styleUrls: [
"./widget-shared.less", // <---- This is the SHARED LESS file. ----
"./widget-two.component.less"
],
template:
`
Hello, I am <strong>Widget Two</strong>. Some of my styles are shared.
`
})
export class WidgetTwoComponent {
// ...
}
Notice that the WidgetTwoComponent meta-data is using the same "widget-shared.less" file as WidgetOneComponent. It then provides its own local CSS file for overrides:
:host {
border-color: magenta ;
}
strong {
color: magenta ;
}
First, just to confirm that this actually works, if we compile this code and run it in the browser, we can see that both of the widgets have the correct styling. They both use the shared styles and then override the colors locally:
Run this demo in my JavaScript Demos project on GitHub.
But, the main point of this exploration is look at the compiled assets. And, if we look at the compiled "main.js" file, we can see that the shared CSS file was turned into a single module that is subsequently loaded into each Widget component:
This is one of those things where someone will read this and inevitably ask, "How else did you expect it to work?" Which is a totally legitimate question ... if you are very familiar with Webpack and an asset compiling pipeline. But, for those people, like myself, that view Webpack primarily as a blackbox, composed of magical spells, eye-of-newt, and the blood of the unborn, the thick abstraction allows for many incorrect assumptions to be made.
And, in this case, I made a poor assumption and never tested it; until today.
That said, now that I understand how shared styleUrls are actually compiled down into the deliverable assets, I think it totally changes the way I want to compose my CSS. Instead of trying to group shared CSS rules higher-up in the component tree and then letting them bleed into child components, I want to start breaking the CSS down into little module files. Then, explicitly include those shared CSS modules into any component meta-data that needs them. To me, this has three major benefits:
It makes it abundantly clear where all styling within a component is coming from. Nothing, except for global resets and fonts, is bleeding into a component - all style properties are clearly linked.
It makes it abundantly clear if an application is still using a given CSS module. The file URL is either being included somewhere or it's not - no more wondering if you can delete a "seemingly dead" CSS module.
It removes the need for "/deep/" or "::ng-deep" selectors in the shared CSS rules since each include of the shared CSS module will be annotated with component-specific attribute-selectors (when using simulated view encapsulation).
It's always so interesting to see how a poor assumption can become a large blocker in the way one approaches a problem. And, it's always exciting to see how correcting that assumption can radically change the path forward. I am sure there is a larger existential lesson to be learned here; but, for the time being, I'm just thrilled to finally understand how shared styleUrls work in my Angular 5 components.
Want to use code from this post? Check out the license.
Reader Comments
What happens if you use the webpack extract text plugin?
Does webpack produce a shared selector?
Very interesting either way :)
@Leon,
Interesting question. I assume that's the plugin that sort of bubbles up text outside of the modules. I am not sure that it would work. Ultimately, Angular still needs to have some reference to the styles in the `@Component()` meta-data for the component. So, I am not sure it can be auto-magically factored-out like that.
But, I'm fairly limited in my understanding of Webpack. I basically have a config that works for R&D, and I haven't gone much past that.
@All,
Now that I've been recently thinking about the emulated encapsulation attributes a bit more in Angular, I wanted to do a follow-up sanity-check to see how CSS module scoping interplays with shared CSS files:
www.bennadel.com/blog/3515-sanity-check-shared-style-urls-and-emulated-encapsulation-attributes-in-angular-6-1-10.htm
The result is fairly obvious in retrospect: the shared CSS declaration is duplicated, oncer per Component Type, with each Style block being given the appropriate attribute scoping. But, it was good to see it "on paper" to help build up that mental model.