Styling Components with the Shadow DOM

First up, the Shadow DOM is not real - yet. Unless you want to restrict yourself to Chrome and Android Browser, we can't use it at time of writing. However, Angular does a reasonable job of polyfilling some of the features to give us a taste of what it might be like.

The Shadow DOM is a proposed future browser feature that will allow us to create separate regions of a webpage that work like separate documents embedded in the main document. It's being pushed by Google, but has precisely zero traction in other browsers at time of writing

This is an attempt to make HTML pages more modular. You might insert a widget into a page, and expect it to come with it's own styles that don't apply anywhere else.

It is rather like an iFrames, except the HTML is embedded in the document.

Support is limited

The Shadow DOM is currently only supported by Chrome and Android Browser, and may never gain wide adoption. See the current support tables here:

http://caniuse.com/#feat=shadowdom

Chrome uses the shadow DOM to style the insides of form elements for example. Angular mocks some of the features of the shadow DOM, so that we can use them today.

Adding styles in Angular

We add styles to our component by passing a styles attribute to our component's constructor or decorator.

var AppComponent = ng.core
.Component({
selector: "app",
template:
`
<div>hello from the shadow DOM</div>
`,
styles: [`
div {
background:red;
}
`]
})
.Class({
constructor: function() {}
})
document.addEventListener('DOMContentLoaded', function() {
ng.platform.browser.bootstrap(AppComponent, [])
});

This generates namespaced styles

If we look inside the head section of our generated DOM, we see a new style tag like this:

<style>
div[_ngcontent-frc-1] {
background:red;
}
</style>

The key thing is this: [_ngcontent-frc-1]. This is a CSS2 attribute selector. It will select only elements that have an _ngcontent-frc-1 attribute. If we look inside our DOM at the component, we'll find a matching attribute has been appended to our template:

<app _nghost-frc-1>
<div _ngcontent-frc-1>hello from the shadow DOM</div>
</app>

Angular is using random generated attributes to namespace portions of the DOM, so the styles only apply to those components. This gives us much of the functionality of the shadow DOM in non-supporting browsers.

Exercise - Shadow Dom

In this exercise, we are are going to add styling to this little user tab bar component:

ng.core.Component({
...
template:
`
<ul>
<li>
<a on-click="tab='history'">history</a>
</li>
<li>
<a on-click="tab='info'">info</a>
</li>
<li>
<a on-click="tab='edit'">edit</a>
</li>
</ul>
`
...
});

Your styles might look something like this:

ul, li {
margin:0;
padding:0;
list-style:none;
}
li {
float:left;
}
a {
display:block;
padding:10px 20px;
background:#f88;
border-radius:10px 10px 0 0;
}

Extension, custom styles

We are going to extend our tab pane, so that when we click on a tab, that tab will become highlighted. We will do this by adding a class to the selected tab.

You can add a class to an element by binding to the class property, like so:

[class.selected]="tab=='edit'">

This will add a class of selected if the expression evaluates to true.

Add in the class binding, and apply some styling like this:

.selected a {
background:#f00;
color:%fff;
}

Now you should have a tab that changes colour when it is selected.

Hard Extension

Split your tab bar into 3 tab button components. These should be parameterised with a name, and should emit 'tab' events when they are clicked.

Make it so that your tab bar emits a tabSelected event when one of the tabs is clicked. Now add three divs beneath the pane that are hidden until the corresponding tab is clicked.

Disabling or going native

If we are only targeting Chrome or Android: perhaps we are building an mobile app for example, we can turn on full native shadow DOM support.

We do this by adding an encapsulation attribute: encapsulation: ng.core.ViewEncapsulation.Native. This is just a constant. It has the value 1.

It looks like this:

var AppComponent = ng.core
.Component({
selector: "app",
template:
`
<div>hello from the shadow DOM</div>
`,
styles: [`
div {
background:red;
}
`]
})
.Class({
constructor: function() {}
})

Now our DOM looks like this:

<app>
#shadow-root
<style>
div {
background:green;
}
</style>
<div>hello from the shadow DOM</div>
/#shadow-root
</app>

That #shadow-root specifies a new shadow DOM. You can see the styles are nested inside that shadow root.

Exercise - Enable the shadow DOM

Turn on the shadow DOM for your tab bar application. In Chrome, use the inspector to inspect your component. Notice how it is encapsulated with it's own style attributes.

Use CSS to apply a style to content the page. Perhaps make the text red with color:red. Notice how the page style doesn't affect the shadow DOM.

comments powered by Disqus