I have a pretty easy rule when it comes to using NG-Deep on my projects, “if you think you need it, I would think a little harder”. 9 times out of 10 when I see people using the ng-deep modifier, it’s because they want a “quick fix” without really thinking through the consequences. And in the majority of those cases, the use of ng-deep comes back to bite. I want to talk a little more about NG-Deep “bleeding”, and how the lazy loading of styles in Angular often hides the issue until things are already in production.
What Is NG-Deep?
NG-Deep is essentially a CSS pseudo element that allows you to “break” CSS encapsulation. For example let’s say that I have a very simple component like so :
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-first-parent',
template: `<app-child></app-child>`,
styles : ['h1 { color:red; }']
})
export class FirstParentComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
Notice a couple of things, that the template is simply a component called “child”, and on this particular page, I want all H1 tags to be red. Well you may think that if I create the child component like so :
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-child',
template: `<h1>This is a child component</h1>`,
styles: []
})
export class ChildComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
We might at first suspect that the H1 tag is going to be red. But if we view it in a browser, it doesn’t work!
When we check the page source for our styling, we can see it looks like so :
h1[_ngcontent-iqb-c1] { color:red; }
The _ngcontent is our view encapsulation taking hold. Because we have put our H1 styling into our FirstParent component, it’s limited that styling to *only* that component. Because our H1 is actually in a child component, we are a bit shafted.
But hold on, we’ve heard about this amazing thing called ng-deep that basically removes encapsulation for components in Angular! Let’s try it!
If we change the styling of our FirstParent component from :
styles : ['h1 { color:red; }']
To :
styles : ['::ng-deep h1 { color:red; }']
Does everything work?
It does! And when we check the source code we can see that the styling has had the view encapsulation removed and it’s just a plain h1 style now!
<style>h1 { color:red; }</style>
So everything works right?
Adding Another Component
Let’s add another component that’s almost identical to the first. We’ll call it “SecondParent”.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-second-parent',
template: `<app-child></app-child>`
})
export class SecondParentComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
This one is identical to the first, except that we don’t have any styling for our H1s. We instead just want to use the default styling.
We will also add some routing so that if we go to /firstparent, we are taking to the FirstComponent, and /secondparent goes to the second. The routing looks like so :
const routes: Routes = [
{ path : 'firstparent', component : FirstParentComponent},
{ path : 'secondparent', component: SecondParentComponent}
];
For this to work, we want a button on the FirstParent that goes to the Second, and vice versa. To make things easier for ourselves, we want to change the FirstParentComponent to have the following template :
template: `This is the first parent. <br /><app-child></app-child> <a [routerLink]="['/secondparent']">Go To Second Parent</a>`
And the SecondParentComponent should have :
template: `This is the second parent. <br /><app-child></app-child> <a [routerLink]="['/firstparent']">Go To First Parent</a>`
While theoretically the template changes aren’t needed, for this next little demo it will make things easier to see.
Let’s try a couple of straight navigation options.
If I go directly to /firstparent, I see :
This is correct. We get the red H1 tag.
If I go directly to /secondparent (By direct I mean typing it in my browser and not clicking the link), I see :
Also correct, we have the black H1 and not the red. Perfect!
Let’s try something else, if we go directly to /firstparent, then click the link to go to the SecondParent, what happens?
So.. When we go direct to /secondparent, everything works fine, but if we go to /firstparent, then navigate via Angular to the /secondparent, it all goes wrong. Why?
Well it’s actually a simple explanation.
CSS in Angular is LazyLoaded, that means that any styles for a particular component are only actually loaded when that component is itself loaded. For us that means that we only get our special ng-deep rule when FirstComponent is actually loaded, but if we go direct to the SecondComponent from our URL bar, then it doesn’t need to load that CSS and so doesn’t.
The reason I want to point this out is because in the majority of cases where I see ng-deep go wrong, it’s been incredibly hard to track down bugs where sometimes you have to go through a series of pages to recreate the bug. In our example, imagine if a tester/QA logged a bug that said when they went to /secondparent, the text was red. Well if we just tried to recreate it by going directly to that page, we wouldn’t see the issue!
At the crux of it though, we see that using ng-deep the way we have done causes big issues because we are essentially writing an H1 rule to the global stylesheet. It’s the exact issue that view encapsulation tries to fix, but then gives us the tools to wreck it anyway.
Working Without NG-Deep
Let’s look at some ways to work without NG-Deep, or more so ways in which we can limit our exposure to bugs like above.
Using :host
First up is my favourite, and one that Angular actually recommends, and that’s prepending any ng-deep rule with the :host modifier. We have a great article on how to use :host here! But in simple terms, if we change our rule inside FirstComponent to look like so :
styles : [':host ::ng-deep h1 { color:red; }']
What we are essentially saying is that we still want to go “deep”, but only when we are inside this particular component. The rule itself when written to the page looks like :
[_nghost-qxj-c1] h1 { color:red; }
Where the _nghost is the FirstComponent, and it’s saying any H1s inside this component can be red, but any H1s inside any other component will not be affected.
Being More Specific
If for some reason you don’t want to use :host (Or can’t), then another option is to simply be more specific in your rules that are going to be global. For example if we changed our template and styling inside FirstParent to look like so :
template: `This is the first parent. <br /><div class="first-parent-wrapper"><app-child></app-child></div> <a [routerLink]="['/secondparent']">Go To Second Parent</a>`,
styles : ['::ng-deep .first-parent-wrapper h1 { color:red; }']
So notice how we have now wrapped our child control in a very specific class, that we can then use for our styling. Even though this rule will be global, it’s unlikely (But not impossible), that someone somewhere else uses the same “first-parent-wrapper” class. But again, you are essentially breaking view encapsulation and banking that no one else uses this class name anywhere else.
Passing Variables
Of course the final option, which should be pretty obvious, is that you can ofcourse pass styles or even switches to a child component. So you can create an input parameter for your child component called “headerText”, and then pass in the color you want it to be. This does have it’s limits though in that generally you are looking for a direct parent to child relationship, and you don’t want to be passing around styling several layers deep. But it’s an option!
Work Without It
This isn’t really a solution but one that always pays to keep in mind. Use of NG-Deep should be a last resort. It should be used when you really can’t find any other way to achieve what you are doing. You’ve asked on stackoverflow and on the Angular Github tracker, and there’s just no other possible way to do things without NG-Deep. Even then, you should use one of the above methods to limit your exposure.
💬 Leave a comment