loading...

Angular 10 - Avoid using ::ng-deep (ngdeep)

jwp profile image John Peters Updated on ・3 min read

Problem

Attempts to get CSS specificity within the View Component CSS sheet often fail. Our Style markup just can't go deep enough to find the element. We know we're doing it right because our Javascript querySelector works. Trying the same selection in CSS of the component just fails!

Environment

We used SCSS for the core, but all Views were using CSS.

Background

First, what is deep? I found issues when attempt to override stylings mostly for Material Components but, have had my own components present challenges when reused elsewhere. Let's just call "Deep" any styling not directly related to the current component.

I believe deep CSS querySelectors within a View's component style, are being ignored by Angular's view encapsulation rules, and I don't know why, nor want to continue hunting it down.

Solution

If we want addressibility to any style anywhere in the project we simply move to the root level SCSS style sheet to accomplish it.

Perhaps it works so well because it's in the root which bypasses View Encapsulation rules.

Just don't use NG-Deep; it kind-of sort-of works but all the red flags are out on it and forget going too deep. Just use root level specific SCSS selectors as shown here!

 ng-select {
    padding: 0.25rem 0 0.25rem 0.25rem;
    border-style: none;
    border-bottom: 1px solid $Color-BlueGreen;
    font-family: $Font-Alternate;
    width: 100%;

    .ng-dropdown-panel {
      background-color: red;
      .ng-option:hover {
        background-color: yellow;
        width: 100%;
      }
      .ng-option {
        background-color: white;
        padding-top: 0.3em;
        padding-left: 0.3em;
        cursor: pointer;
      }
    }
    .ng-select-container {
      .ng-value-container {
        .ng-input {
          input {
             // Works every time!
            width: 100%; 
            // Five Levels Deep
          }
        }
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Here's another example of avoiding :ng-deep! This was in the core.scss stylesheet. It worked first time!

app-parent {
  app-child-grid {
    app-child-edit.cdk-drag {
      form {
        div {
          // 6 levels deep from the app-parent
          ng-select {
            width: 101%;
          }
        }
      }
      .className {
        app-custom-control {
          // Still had to override this one
          justify-content: end !important;
          margin-right: 2em;
          .buttons {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(3em, 1fr));
            min-width: 6em;
            margin-bottom: 0.5em;
            button {
              max-width: 5em;
            }
            div[name="faIconSave"] {
              justify-self: end;
              margin-right: 0.7em;
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The demo above shows 6 levels of depth that altered the style on the first attempt! 10 minutes to perfection and addressibility vs. days to try to kind-of sort-of get NG-Deep to work.

How We Arrived at this Solution

Angular's API states that the ng-deep psuedo-class is deprecated.

Furthermore; it states that ng-deep

completely disables view-encapsulation for that rule.

If we use it without the :host pseudo-class, it will make the style-rule global, not a good thing.

There's something odd about Angular view encapsulation which gets style specificity wrong. How do we know? If we write a Typescript QuerySelectorAll we can pull any ID or Class on the page regardless of depth.

But if we use a CSS selector in the component's StyleSheet, looking for the same ID... Angular doesn't find it when the depth is deep. We have never understood why.

This forces us to write Typescript QuerySelectors for our component's ele.NativeElement to narrow the search; but, we don't really want to do that. We prefer all styling in the StyeSheet of the component.

Old Solution Was
If we ignore the ::ng-deep deprecation warning for now, (after all, it's still working in Angular 10); we come up with specific rules following this format.

// Note we don't need the ID
// We just go for the className
// This still allows for cascading

:host::ng-deep.className{
  width:5em;
}
Enter fullscreen mode Exit fullscreen mode

This code functions the same as using a query selector, removing the old class name and adding in the new class name:

let element = 
ele
.nativeElement
.querySelector('.className')
ele.class.remove('oldClassName');
ele.class.add('newClassName');
Enter fullscreen mode Exit fullscreen mode

We can spend a ton of time trying to write more specific CSS selectors (no guarantee that Angular's View Encapsultion) will find them, or we just use this pattern...

:host::ng-deep.className
:host::ng-deep.#IDName
Enter fullscreen mode Exit fullscreen mode

Best Option
We found the best option is to use Less or Sass to build very specific style rules, this works better than ng-deep!

JWP 2020

Discussion

pic
Editor guide
Collapse
lifelongthinker profile image
Sebastian

Maybe I can shed some light on this: Angular works with "pseudo" view encapsulation (Angular shadow DOM). Templates are assigned specific class identifiers. Your stylesheets are then compiled with those class identifiers baked into them. This is Angular's way of mimicking view encapsulation.

When you query the DOM at runtime, you see the final markup composed by Angular with the class identifiers assigned. That's why TD can query the elements but your style sheet selectors don't work. They basically work against two different CSS truths.

Collapse
lifelongthinker profile image
Sebastian

One more thing: I have started refraining from using ng-deep() while :host and :host-context are pretty useful.

Instead, I use global app style includes that basically lie in the component folders and are called 'component.global.less' (my notation). These are included in the global app styles and thus left untouched by the Angular compiler.

Collapse
jwp profile image
John Peters Author

Sebastion; I tried :host-context yesterday, it worked a bit deeper but not all the way into a 4 or 5 level deep component. I may be doing something incorrect in my css selectors. Just not sure yet.

Thread Thread
lifelongthinker profile image
Sebastian

I am experiencing similar issues. It seems the selectors are extremely picky. This is what I do when debugging those issues:

  1. Check if the CSS in question has indeed be compiled into the final stylesheet(s).
  2. Check in the Browser Dev Tools if the CSS in question is being applied to the intended elements, or (if not) where and why they are overriden by other styles.
  3. Try to target said elements through a CSS selector in the global style sheet to see if component isolation is the issue.
Thread Thread
jwp profile image
John Peters Author

Okay just figured out another clue to this puzzle, in the data below this is how the browser style page show the order. Material Components do late CSS binding somehow, it makes things hard to fix because those styles have highest specificity.

.mat-button[disabled], .mat-icon-button[disabled], .mat-stroked-button[disabled], .mat-flat-button[disabled] {
    cursor: default;
}

<style>
.mat-icon-button {
    padding: 0;
    min-width: 0;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    line-height: 40px;
    border-radius: 50%;
}
<style>
.mat-button, .mat-icon-button, .mat-stroked-button, .mat-flat-button {
    box-sizing: border-box;
    position: relative;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    cursor: pointer;
    outline: none;
    border: none;
    -webkit-tap-highlight-color: transparent;
    display: inline-block;
    white-space: nowrap;
    text-decoration: none;
    vertical-align: baseline;
    text-align: center;
    margin: 0;
    min-width: 64px;
    line-height: 36px;
    padding: 0 16px;
    border-radius: 4px;
    overflow: visible;
}

// This was ::ng-deep but only !important made it work

.mat-icon-button {
    width: 1em !important;
    height: 1em !important;
}

They say don't use important but I know of no other way to get my style rules to the top other than setting them in javascript at the element layer which is at the top of rule list. Everything else was intrinsic to the material controls.