In this series we explore the Web Component APIs: HTML Imports, Custom elements, HTML Templates, and the Shadow DOM, seeing how each of these APIs can be used within the context of your Aurelia applications. In this installment, we’ll look at the features Aurelia provides, including content projection, DOM encapsulation, and layouts.
The Shadow DOM
Imagine the following scenario: You’ve found a third party control you wanted to drop into your page. The documentation for the component displays a nicely laid out control, with a color scheme that would fit perfectly into your application. With a feeling of anticipation, you eagerly install the component with NPM and include it into your page layout. Problem is, when you re-load the page you realize that that the styles of a different third party library happen to conflict with the component you’ve just added, pushing the margins and padding way off to the side of the site. If that wasn’t bad enough, as you start typing into the control you realize that none of the key-down events are registering because that same bad neighbor of a third party control happens to take control of this as-well.
Wouldn’t it be nice if we had a way of encapsulating each of the components that we included to ensure a predictable result when including them on your page? Further, if you put yourself in the shoes of the component author, wouldn’t it be nice if you could publish that component without facing the inevitable barrage of Github issues reporting the weird and wonderful ways that developers have found to break your beautiful component with the !important
directives littered throughout their 10-year-old CMS?
In fact, we’re in luck. The Shadow DOM was introduced to solve these encapsulation problems. It provides a way of encapsulating parts of the DOM (known as DOM subtrees) from the main DOM tree. This allows you to create isolated components that you can drop into a page without worrying about styles or behavior being disrupted by styles or JavaScript included in the main page.
Vanilla Shadow DOM
You can use the Shadow DOM with the aid of some polyfills today by using the element.attachShadow
method. You can see an example of this below, where we query for a #componentHost
element and then attach a new subtree to it using the element.innerHtml
method:
If we take this one step further and mix this with custom elements (which we saw in the previous post in this series), we’ll begin by declaring a simple empty info card template <template id="info-card>
. This template takes a message as input which it will render into the template content. We’ll then hook into the createdCallback
method, importing the info card node document.importNode(infoCard.content, true)
, creating a new shadowRoot
node this.createShadowRoot()
and attaching the new info-card
node to the DOM using root.appendChild(infoCardInstance)
. Finally, we register the new custom element with the DOM using the customElements.define('x-info-card', XInfoCard)
method:
If you load this in the browser you’ll notice that the custom element content has been injected under a Shadow root node, meaning that the contents are isolated from any styles applied to the light DOM (the regular DOM outside of the Shadow DOM sub-tree):
Anything inside the #shadowRoot
DOM node is invisible to the light DOM. For example, if we attempted to access the <p>Hello World</p>
node using jQuery we’d receive an empty node list in response. Also if we applied styles to the <p>
tag using CSS in the parent document, our p
tag in the x-info-card
component would also be protected.
Shadow DOM Slots
Apart from isolation, there is another handy feature of the Shadow DOM called slots which allows you to define a placeholder in your component into which you can inject custom HTML. For example, we could define a header
and body
slot in the x-info-card
template as follows:
You can then use these slots, projecting your own custom HTML fragments into each slot location. The following example shows how you could inject a header
and body
fragment into the info-card
custom element:
With the addition of Shadow DOM slots to our example page, the resulting components now show the HTML fragments that we injected into the header
and body
slots. The resulting example looks like this:
The thing that I really like about Shadow DOM slots is the fact that they allow you to create custom elements that behave just like standard HTML elements, where you can add nested HTML below the root custom element node. This is very powerful, as it allows you to create reusable high-level components which you can then compose with more specific components. In the case of our info-card
, we could potentially create an image-info-card
which extends the basic info info-card
but projects image related HTML into the content slots.
Using the Shadow DOM with Aurelia
Aurelia provides four main features that allow you to leverage the Shadow DOM in your projects: CSS as='scoped'
require option, the useShadowDOM
decorator, shadow DOM slots, and layouts.
Scoped CSS
CSS scoping in web components is a hot topic. There generally two requirements for scoped CSS:
- Avoid styles from outside your component being applied to your component
- Avoid styles from within your component leaking outside your component
The first requirement can be met in Aurelia by decorating your view-model (the JavaScript class that backs your view) using the @useShadowDOM
decorator. You’ll see a demonstration of this soon. The second requirement can be met in two ways. The first way (shown in the below snippet) shows how you could achieve this goal by adding the as='scoped'
option to your stylesheet’s require
statement. The problem with this approach is that it relies on an API that is currently only supported in Firefox, so the current applications are limited.
The second way (and the simplest approach I’ve to date) for having styles only apply to your component (without the use of a CSS processor like Postcss) is to include the styles inline in your view. This is the approach we’ll take in our examples.
Using the Shadow DOM
Aurelia components are created by defining view, view-model pairs where the name of the view matches the name of the view-model (JavaScript class). To have an Aurelia component inject it contents inside a shadowRoot
node all you need to do is add the @useShadowDOM
decorator to the view-model class declaration. The view and view-model pair can be created as shown in the below code samples. What’s striking here is that the HTML template in this case is exactly the same with Aurelia as with vanilla web components. The view-model is also very close, but with some neat improvements to the JavaScript API to make it terser:
info-card.html (view)
info-card.js (view-model)
You can see this Aurelia Shadow DOM sample in action at this GistRun.
Shadow DOM Slots
Aurelia’s Shadow DOM slot syntax is identical to vanilla web components. Where Aurelia has an advantage, however, is that you can also use Aurelia’s light-weight data-binding syntax to bind content into the different slots. For example, to bind content into the header and body slots it’s a simple matter of using one-way string interpolation binding. For example, in the below sample root app.html
view file we’ve used two info-card
elements, and we’re binding the header and body content from the app.js
view-model values ${card1Header}
. These could just as easily come from a repeater
if we wanted to render the cards in a list.
app.html – view file
For more information on the specifics of using DOM slots with Aurelia, I’d recommend checking out this great post by a fellow Australian Aurelia developer Dwayne Charrington https://ilikekillnerds.com/2017/07/checking-view-slot-defined-aurelia.
Layouts
Layouts allow you to define multiple top-level pages into which views in your application can be rendered. This allows you to lay out your user interface the way that best fits each page in your application. Within a layout, you can define swappable areas using Shadow DOM slots. A neat use case for layouts is to define a separate layout for a login page (which doesn’t necessarily need a navigation menu) than they layout you’d use for the main page which would include the nav bar and so on. Layouts are kind of a big topic, so we’ll cover them in a later blog post instead.
That just about wraps up the basics of using the Shadow DOM with Aurelia. In the next installment in this series, we’ll create a Bootstrap 4 info-card
component with Aurelia which uses the Shadow DOM to project custom HTML fragments into different card areas. Stay tuned!
Good explanation. Please keep it coming. I am eager to learn about Layout in particular.
Thanks Alain, glad you found it useful :D! Yes, layouts are one of those lesser known features of the Aurelia framework which are very useful for certain scenarios.
Oh my god thank you, I’ve had trouble wrapping my head around some of this even after reading the docs and this was exactly what I needed to read to make it click!
Excellent glad it helped! 😀