Why learn about component level isolation?
The most frequent (and important) task we do while building web applications is componentizaton. We break our interface into small re-usable pieces and divide them into isolated components. If you are a Angular 1 developer, you already know that Angular 1 was good but not good enough to support isolated component level styles. If you are new to the Angular (2,4+) world ( i.e coming from a different framework like React, Ember or even new to front-end), I have some good news. Angular is now not only good enough but actually amazing at handling styles at component level.
For folks who are new to Angular world, "AngularJS" refers to Angular 1 and "Angular" refers to Angular 2, 4 and beyond. This articles talks about Angular and not AngularJS.
Angular's flexible component style management gives you granular control over how you isolated your style pieces. In this blog, we will first quickly look into how Angular provides isolation layer for styles and then deep dive into how it handles style with complex content project. (No idea what content projection is? No problem, don't worry its not really complex. Keep reading!)
Here is a basic Angular component with "styles" embedded right within the @Component decorator.
Let's look at how the my-custom-input.component.html file looks like:
The first thing I would probably want to do to clean things up is to move the styles in a separate file. To do this, you simply want to use the styleUrls property of @Component decorator instead of styles property. Here is how it looks:
Another thing you may have noticed is that I have added encapsulation property and have set it to ViewEncapsulation.Emulated. This is the default value and I am adding it here just to make sure that you know how components behave inherently if you use the default Emulated encapsulation in Angular.
It's not really required for this blog, but if you are interested in learning about different view encapsulation modes in Angular, you can go here
The styles file now lives on their own, here is how it looks now:
Let's inspect elements in the browser to see how it rendered the styles and if there is anything special?
The renderd HTML looks like
and Angular addes special styles in the head of the document
After looking closing into the rendered HTML, we can easily figure out the following:
- The component gets embedded inside a pseudo HTML tag called my-custom-input. This matches the selector property we defined on the component. We can think of this extra parent node as a "host" to our actual component HTML.
- The actual component HTML rendered as is inside the host tag.
What's special here?
The host tag: gets a (random like) attribute _nghost-c0 This special attribute act as an identifier for this host tag. Essentially it tells us 2 things, first , its a "host" tag hence _nghost and c0 its the ID. This ID will be used by all tags inside of this host.
- All inner component HTML tags also get a _ngcontent-c0 attribute which kind of ties it to the host. It indicates that all these inner tags ( div and input in this case) are part of ( content of ) the outer host c0
Can we apply styles directly to a host tag?
Yes we can ! How ? Just by adding a special pseudo :host definition in css . Let's do it:
:host is a special styling modifier which adds style to the host tag. Let's see quickly how our component looks like after rendering:
Check the browser at this time, shows that the styles were applied directly on the host tag and are working perfectly fine.
Here is a working example for whatever we have accomplished uptil now:
As we discussed, Angular adds a <style> tag in the <head> for each type of component. The important part to notice here is it also uses the ( auto generated ) special attribute [_ngcontent-c1] to it. What Angular is essentially doing is scoping the styles of each component (type) by using these auto generated identifiers per type. What do I mean by "per type"? If we use the same component 10 times on the page, Angular won't add the same styles 10 times. It adds the style once per type of component.
Everything seems to work uptil now. A lot of times when we build reusable components, we would like the consumer of our component to be able to inject any kind of HTML templates into our custom component. For example, we want all our input types ( text, password, email e.t.c) to follow the same styles of our input tag i.e. have a red border line at the bottom. We can make use of <ng-content>, a Angular construct to make your components more reusable. &ng-content> allows the consumer to inject any content into the component template. Here is what we will change in our component .ts and .html file file
And here is what the consuming application needs to do:
No brainer, eh ? This is how we do content project in Angular. The consumer of my-custom-input is now able to inject anything inside of the host tag. Here the consumer is injecting a <input type="text"> into our my-custom-input which replaces the <ng-content> in the component template.
What is content project?
It's when you want to make your component flexible enough to inject any kind of content into its HTML and also compile it
into Angular's construct.
Why would I want to do it in this particular scenario? As a component architect, we may believe that we don't have to create new custom input components for each type of input like simple text, email, number etc. We want to use the same custom input component for all types of generic input html elements. The way we can do it is change out internal component HTML to use <ng-content>
I am not going into the details of how content project and ng-content works. We will save those topics for a detail intro for another day.
Benefits?Now in the consuming application, we can use my-custom-input to inject whatever (for now we wil just inject different types of input as we intended) Let's see how the consumer will look like injecting different types of input tags into the same my-custom-input
So far so good. We are now able to inject any type of generic input into our component input component. Let's look at a Plnkr to see if the styles are still working as expected?:
Oh boi! There is no red bottom border on each of the input? What happened to our styles? Our component style file is still the same but the component is not able to find a input inside of the host my-custom-input. Content project has broken our basic css selector :host. Before fixing this , let's first understand why this happened and to see if this a bug in Angular?
Here is how our new rendered HTML looks like:
If you look at the rendered HTML, you can see that the injected input tags do not have any _ngcontent-cX attribute. This means the injected contended did not get any identifier to tie it to its host.
Is this a bug or a feature?
It's actually a Angular feature. Angular would like to maintain every component's isolated style. At this point, we are injecting a simple input tag but we could be injecting a complex custom tag as well. That custom tag may have its own style. Angular, for maintaing the custom component's isolated style, did not give it any identifier tying it to the host. This may sound crazy at first but this is extremely powerful. Any component you will inject to any other component having ng-content will maintain it's own isolated style. Pretty cool !
So this is how Angular's style resolution mechanism works by default and the reason why it works like that. Since in our case, we don't want this behavior, rather we want to apply our structural styles to every injected input. Let's looks at how we can overcome this :
To fix this problem, our most immediate solution one could come up with is to scope our input styles with :host. Let's try if that works:
Unfortunately, this doesn't work. Here is a working Plnkr which is not working as we expect it to work:
To overcome this style isolation barrier ( and benefit for some other scenarios ), Angualr gives us another little beatuty. Welcome to the /deep/ modifier. Let's apply this magic modifier to see if this can help:
Here is a working Plnkr:
This kind of seems to work. It solved our problem of maintaining specific styles to input injected inside of custom input component. However, it created a new problem. It kept the style isolation barrier within our component, but broke the isolation barrier for general <input> outside our component. The new styles are now leaking outside of our custom input component and getting applied to all inputs even outside the component because of the magic /deep/ modifier. If we glance over the style section added in head of HTML, we can see that this specific style is directly applied without any scoping:
Can we do better? Obviously! Don't you love & trustAngular at the same time? We have you covered.
Here is a final solution to this very particular problem. We gotta use the magic :host pseudo selector in combination with the amazing /deep/ modifier.
In this case, Angular scopes the input styles with a particular component identifier:
Here is the final working example on Plnkr
So even when the input is projected into our component via ng-content, our styles are working as expected. This assures that the Angular component styling mechanism really works!
Didn't you like it? Do you think Angular's styling mechanism is powerful enough to fulfil your needs for building highly reusable components? Leave a line below.
Feel free to leave a comment, question, suggestion and corrections. Until next time, Happy learning!