Moving Core Application CSS Styles Into The Root Component In Angular 2.0.0
By default, pretty much all of my JavaScript demos have a "demo.css" file that I link-to within the "index.htm" page. But, I'm starting to wonder if this makes sense for my Angular 2 demos. In Angular 2, we can easily use emulated style-encapsulation within our component directives; which makes me reconsider how much CSS is actually "owned" by the index page itself? And, should most of that CSS be moved into the component definition?
Run this demo in my JavaScript Demos project on GitHub.
In my Angular 2 demos, the index page has basically nothing on it. Usually just an H1 tag and the "my-app" root Angular 2 component. This is the only visibility that the index page has into what is being rendered on the page. The rest of the content is completely encapsulated inside the Angular 2 ecosystem. So, perhaps it makes sense that the only thing the index page knows how to render is the Body tag and the H1 tag?
Here's what this might look like:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Moving Core Application CSS Styles Into The Root Component In Angular 2.0.0
</title>
<style type="text/css">
/*
Technically, the only non-app styles in this demo would be the BODY and the
H1 tags (and any external drivers for the MY-APP element, for example if it
needed to be positioned). The rest of the content and its relevant styles
belong to the app itself.
*/
body { /* ... */ }
h1 { /* ... */ }
my-app { /* ... */ }
</style>
<!--
While the use of fonts may "belong" to the app, common font usage poses an
interesting problem. When using remote font providers, like Google Fonts, the
font file is loaded from a different domain and it's loaded based on the current
user-agent. As such, the content of the remote CSS file cannot be known ahead of
time. And, what's more, Angular will skip any remote file URL in the "styleUrls"
component meta-data. All to say, fonts have to be loaded in the root page by the
current browser consuming the application.
-->
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700"></link>
<!-- Load libraries (including polyfill(s) for older browsers). -->
<script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/core-js/client/shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/zone.js/dist/zone.js"></script>
<script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/reflect-metadata/Reflect.js"></script>
<script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/systemjs/dist/system.src.js"></script>
<!-- Load the Web Animations API polyfill for most browsers (basically any browser other than Chrome and Firefox). -->
<!-- <script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/web-animations-js/web-animations.min.js"></script> -->
<!-- Configure SystemJS loader. -->
<script type="text/javascript" src="./system.config.js"></script>
</head>
<body>
<h1>
Moving Core Application CSS Styles Into The Root Component In Angular 2.0.0
</h1>
<my-app>
Loading....
</my-app>
</body>
</html>
To illustrate what the index page is styling, I'm using an inline Style block as opposed to a linked stylesheet. And, as you can see, it just has (mock) definitions for the body, h1, and my-app elements. Because, this is all the index page knows about - this is the entirety of its insight into the working of the page.
Now, I'm also linking to a remote Font file from Google Fonts. It turns out, remote Fonts pose an interesting problem. Most CSS styling can be statically understood at compile time, when Angular is stitching together all the things. But, remote font files are different; not only is the content of the font file driven by the User-Agent of the requesting browser (I believe); but, its remote nature means that it can't be known at compile time. As such, I believe that remote font files still need to be owned by the index page. That said, I may be thinking about this incorrectly - if so, please let me know!
As we move CSS out of the index page and into the domain of the Angular 2 components, we have to think about "base styles." Meaning, the general look and feel of the application itself, as opposed to the per-component specific stylings. Things like font-family, font-color, and margins for generic content.
For context, let's look at the root component:
// Declare ambient module definition so TypeScript doesn't complain.
// --
// TODO: Figure out how to move this to typing files.
declare var module: { id: string };
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
moduleId: module.id,
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<h2>
Welcome to Thunderdome!
</h2>
<p>
<a (click)="toggleChild()">Toggle child component</a>.
</p>
<my-child *ngIf="isShowingChild"></my-child>
`
})
export class AppComponent {
public isShowingChild: booldean;
// I initialize the component.
constructor() {
this.isShowingChild = false;
}
// ---
// PUBLIC METHODS.
// ---
// I show or hide the child component based on the current state.
public toggleChild() : void {
this.isShowingChild = ! this.isShowingChild;
}
}
Here, you can see that the root component has a "P" tag in its template. It's possible that this particular P tag should have very specific styling; but, it's much more likely that this P tag should render like every other P tag in the application. And, if the index page won't tell us what that's supposed to look like (because it doesn't know), the root component of our application must.
Of course, the [default] emulated encapsulation scopes all style rules to the current component. Fortunately, Angular gives us a work-around with the "deep" operator:
>>>
This "deep" operator allows us to define CSS selectors that purposefully bleed into the descendants of the current component. When we include this in our CSS selectors, no scoping will be applied to selectors that come after the deep operator. To see this in action, let's look at the CSS file associated with the root component. You'll notice that this file contains component-specific styles as well as "base" styling to be inherited by the rest of the application:
NOTE: In retrospect, I should have probably broken these two concerns up into two different CSS files and loaded them individually in the styleUrls component meta-data.
:host {
border: 1px dashed #AAAAAA ;
border-radius: 5px 5px 5px 5px ;
color: #333333 ;
display: block ;
font-family: "Open Sans", sans-serif ;
padding: 20px 20px 20px 20px ;
}
/*
NOTE: Angular will prepend the component unique identifier to the :first-/:last-
child pseudo-selectors. So, they end up like "[ _ngcontent-id_3z ]:first-child".
*/
:host > :first-child {
margin-top: 0px ;
}
:host > :last-child {
margin-bottom: 0px ;
}
/*
When CSS is rendered for the Angular components, it adds a unique identifier to each
CSS selector. This applies to compound CSS selectors as well. So, for example, if we
had:
:host h2 {}
... Angular would actually render it as something like:
:host[ _nghost-id_3z ] h2[ _ngcontent-id_3z ] {}
... which means our CSS won't bleed down into the nested components. Of course, in
a root app component, this might be exactly what we want for a reset or a base CSS
definition. To get around this, Angular provides a way to turn off the unique
identifier on compound selectors: "/deep/" or, as an alias, ">>>".
*/
:host /deep/ h2 { /* example using /deep/. */
color: #555555 ;
}
:host >>> h3 { /* example using >>>. */
color: #777777 ;
}
:host >>> p {
font-size: 18px ;
}
:host >>> a {
color: red ;
cursor: pointer ;
text-decoration: underline ;
user-select: none ;
-moz-user-select: none ;
-webkit-user-select: none ;
}
Here, you can see that we are defining "deep" styles for the h2, h3, p, and a elements. These are the "base" styles for the application; and, they are provided by the root component because the root component is the only component that can truly claim to have a holistic understanding of how the application should look and function.
To test this cascading of styles, I included a child component that uses some of those base tags:
// Declare ambient module definition so TypeScript doesn't complain.
// --
// TODO: Figure out how to move this to typing files.
declare var module: { id: string };
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
moduleId: module.id,
selector: "my-child",
styleUrls: [ "./my-child.component.css" ],
template:
`
<h3>
What a Wonderful Day!
</h3>
<p>
Good morning my friend.
</p>
`
})
export class MyChildComponent {
// I initialize the component.
constructor() {
// ...
}
}
And, in case you're curious, the my-child component has its own CSS file, of course:
:host {
border: 1px dashed #AAAAAA ;
border-radius: 5px 5px 5px 5px ;
display: block ;
padding: 20px 20px 20px 20px ;
}
/*
NOTE: Angular will prepend the component unique identifier to the :first-/:last-
child pseudo-selectors. So, they end up like "[ _ngcontent-id_4a ]:first-child".
*/
:host > :first-child {
margin-top: 0px ;
}
:host > :last-child {
margin-bottom: 0px ;
}
Now, when we render this page, we can see that the "deep" styles provided by the root component cascaded down into the child component:
As you can see, each component has its own specific styling; however, the root component successfully provided "base" styling that created a cohesive look and feel for the application.
Now, you might take objection to the idea of "cascading" styles. It seems that in recent years, the "C" in "CSS" (Cascading Stylesheets) has come under much criticism. Personally, I'm not good enough with CSS to have much of an opinion on the matter. That said, I still think it's worthwhile to consider the ownership of the CSS; no matter how you architect your styles, it would seem that the index page should know next to nothing about it. And, it's nice that Angular 2 makes this fairly easy to do.
Anyway, just something I've been noodling on lately. I'm trying to take my older thinking about CSS and apply it to a modern, componentized mental model. Still very open to suggestions on this topic, to be sure.
Want to use code from this post? Check out the license.
Reader Comments
Interesting idea and was thinking about this. But also the performance and download size benefits of this concept need to be test for huge active user website and client device compatibility.
I find myself deeply uncomfortable with the Angular 2 philosophy toward CSS. If you're familiar with the CSS Zen Garden project (http://www.csszengarden.com/), the idea is that you can completely change the look and feel of a web page based on the CSS. The idea of separation of content from presentation is a "best practice" that goes back decades.
I guess it's no surprise that the people who brought us Bootstrap, the Framework that virtually forces developers (willing to use the Boostrap grid system) to adulterate their structural markup with presentational markup, would then go further to produce a new framework that tightly couples the CSS to the component. There doesn't seem to be any way to dynamically specify that you might want to use a different styleUrl under different circumstances. When you use Angular-CLI, the situation is even more dire, because WebPack sucks up the CSS and compiles it into the package.
This is problematic not just on a design level, but also on a pragmatic level. When we use separate style sheets that don't get processed into JavaScript, it's possible to right click on any element and look and see down to the line number where any given CSS property is coming from using "inspect element." I'm not sure what problem they thought they were solving by introducing this tight coupling, but the problems it creates in terms of denying developers most of the power of CSS seem like they outweigh the solutions at this point.
@Sarin Na,
Yes, agreed. Right now, all of my R&D takes place in the browser with each file being loaded individually by System.js. Ultimately, however, I believe this would all be compiled down and the CSS would be inlined ... I think. Though, I am not 100% sure.
@Amy,
Oh, I'm very familiar with CSS Zen Garden. When I was first learning about CSS, I was constantly checking out the Zen garden to see how people were changing the page so drastically with a common markup. I remember being frustrated that new designs didn't come out more often ;P
I definitely understand your point of view. And, to be honest, I don't feel strongly enough one way or the other to argue any particular point. I believe that the component-level CSS features might be more for "base" styles, in the same way that your browser has base CSS styles for its components (Input, Select, Div, etc.). The things that give it its basic shape and behavior.
I think its more about simulated "shadow DOM" than it is about "all CSS". And, the simulated shadow DOM is just driven (at least in my experience) by an attribute selector like:
div[ random_think_it_generates ].foo { ... }
So, I think you can still override that in a secondary style sheet if you want to. I can't remember what kind of Specificity an attribute selector has; but, this is an interesting thought. I'd like to do some R&D on overriding these styles and get back to you.
It looks like to me that even if you use styleUrls that Angular 2 reads in those files and translates them to JS. I would suspect that the CSS applied in JS would "win," since I would think it would be applied after CSS from an external style sheet. If that's the case, specificity really would not matter, because the JS styles are always going to be the ones that are used.
It just seems to me that a pseudo shadow dom would be the edge case that only a few developers who are releasing components to other developers would care about. Being able to separate the markup from the layout has been best practice for over a decade with good reason, and for that not to even be supported in Angular 2 seems a poor design decision.
@Amy,
I actually just ran into an interesting case of "CSS Specificity" while trying to think a bit more deeply on your concerns:
www.bennadel.com/blog/3203-exploring-css-specificity-with-shadow-dom-styles-in-angular-2-4-1.htm
... the short of is that the simulated shadow-dom uses Attribute selectors. Which have greater specificity than Type selectors. Which means that it becomes difficult to override the default styles provided by the Component's host element.
This is not a problem I know how to solve at this point.
To be clear, though, there's nothing forcing you to use the shadow DOM functionality. You can certainly continue to have the component provide the markup and then to provide all the CSS in a completely separate file. You have opt *into* the shadow-DOM stuff. So, I guess you can find an approach that makes you the most comfortable.
@All,
It turns out, moving all CSS into the shadow DOM actually solves some CSS specificity problems as well:
www.bennadel.com/blog/3204-solved-css-specificity-and-shadow-dom-overrides-in-angular-2-4-1.htm
The more I think about this, the more I think it makes sense. If you think about what the shadow DOM is, it's a means of encapsulation and protection. It encapsulates the implementation details of the component and protects the internal structure from the CSS of the parent page.
Well, if you think about that in terms of the Angular 2 "root component", the entire app is in the shadow DOM. As such, it makes sense for all CSS associated with the entire app to be inside the shadow DOM as well.
@All,
This morning I sanity-checked the behavior of "styleUrls" in the Angular component meta-data. Turns out, I had been making a very poor assumption - that shared styleUrls would create duplication in the compiled assets. This is, in fact, not true:
www.bennadel.com/blog/3372-sanity-check-shared-style-urls-are-only-compiled-into-angular-5-0-1-once.htm
... the shared styleUrl just gets compiled as a single module and the required into each consuming component (at least in the way I am compiling with Webpack). This is very exciting because it means that much of what would have been higher-up in the component tree can actually be moved down into various components where it becomes much more clear and locally-scoped from a view-encapsulation standpoint.
I have two different pages customer portal and admin portal, and I have both themes purchased. My question is how to integrate these two themes into angular 5 app.
@Hamid,
To be honest, I am not super clear on how to deal with themes in either Angular or, really, CSS in general. It's not something I have a lot of experience with. From what I have seen people discuss, though, some people deal with this using Variables in a CSS pre-processor (like LESS or SASS). I've also moved away from "deep" selectors, since they seem to be deprecated everywhere and will be removed one day. Instead, I've been experimenting with used shared .less files in my component styleUrls:
www.bennadel.com/blog/3372-sanity-check-shared-style-urls-are-only-compiled-into-angular-5-0-1-once.htm
Angular allows you to pull in multiple URLs for each component's CSS. This means that you can pull in a shared CSS file into each component, and it will only be compiled once (although it will be included in each STYLE block that gets injected into the Head of the HTML page).
This works well for small, shared pieces of style; but, to be honest, it feels a bit janky for global styles.
I wish I had better advice here. It's not something I have a good grasp on yet.