What’s that?
What’s a content scaffold component? It’s a re-usable component that includes boilerplate markup, styles, and assets. Distinctively, a content scaffold provides something like this:
When it’s used, a developer will insert specific content to create something more like this:
Why is it useful?
From includes, to templates, to boilerplate generators, web developers have always needed ways to re-use site-wide layouts on different pages without having to copy and paste or rewrite code. While the advent of the SPA (Single Page Application) has somewhat reduced the need to duplicate top level application layout code for some developers, similar practices are still applicable to interior, repeating content panels.
Also, for OpenTable’s restaurant product Guest Center, we have another reason for being interested in quickly scaffolding site layouts and content panels. Our restaurant team is reinventing a large, feature-rich, traditional software system as a web-based, cloud-hosted application. Our backlog of features is, as a result, pretty mature, and it’s understandably one of our goals to bring the functionality our restaurant partners need to the web quickly.
Those features are, it turns out, generally pretty self-contained and highly task-specific. So, we’re adopting a micro-app approach in order to achieve the velocity we want. In our micro-apps, each task-specific feature (like managing a floor plan, or viewing reservations) is a separate SPA. This allows small, distributed teams to work independently and with high focus on features with minimum stepping-on of toes.
It became immediately obvious that we needed an efficient way to distribute our top-level product layout (especially navigation) between separate teams, each team with a separate codebase in a separate repository. Ideally, the component we distribute should have good developer ergonomics and, a future dream, minimize framework dependencies that impose rigid constraints on individual teams.
How, though?
That’s what a content scaffold is and why we use them — what about how to make them? There are probably almost as many options to accomplish our goal as there are developers. However, given the progress of the Web Components specifications, Web Components are likely, in the future, to be the ubiquitous and native way to create HTML components. That makes makes Web Components an especially appealing choice. We can experiment with them today, even in browsers which have not yet implemented them, using polyfills.
Web Components
A Web Component is created using one or more of the Web Component specifications: Custom Elements, HTML Templates, Shadow DOM, and HTML Imports. Web Components enable us to do amazing things. Take, for example, the humble input
tag.
<input type="range" min="1" max="8" />
To use this single tag takes under a second, but it comes with a lot of utility. The element exposes a public API for relevant tasks such reading its current value
. It renders a graphical user interface with clear affordances for a selecting value in a range: a slider with a track and movable handle.
The most natural way to share custom functionality and graphical user interfaces on the web would have to be emulating native elements, and that is exactly what Web Components allow us to do.
The Shadow DOM, for example, is a major contributor to how the range input
works. When “Show user agent shadow DOM” is turned on in Chrome Developer Tools, it’s possible to inspect the hidden DOM that actually builds the range input
.
<input type="range" /> #shadow-root (user-agent) <div id="track"> <div id="thumb"></div> </div>
The Shadow DOM contains the boilerplate required to draw the range input
slider’s track and handle. It’s styled, in part, by user agent stylesheets bundled with the browser, and it can be interacted with and functionally enhanced with JavaScript.
The input
tag doesn’t serve as a container for arbitrary content, however. What we’re looking for in a site layout scaffold is an element more like a div
, which wraps HTML content.
<div> anything goes here </div>
But a div
takes all its contents in one lump, and it’s invisible until we style it. We want a tag with all of this:
- A gray header with a white logo into which we can, optionally, add our own controls
- A gray menu bar into which we can, optionally, add our own menu
- A darker gray body area into which we can add our own body content
- A footer with a subtle copyright statement
So we obviously don’t want to have to copy and paste the header, logo, menu, body, footer, and copyright every time. Maybe we want something a little more like a fieldset
, which has default styles and also has a sort of optional associated “sub-tag”, a legend
which also has default styles.
<fieldset> <legend>legend content</legend> <p>paragraph content</p> </fieldset>
Getting Started
Before we get into new things, we should write the boilerplate HTML that will be built into our content scaffold (which we’ll hereby dub ot-site
). For now, we’ll mark the places we want to be able to insert content with comments and come back to them later. We’ll also write styles so that this boilerplate looks the way it is supposed to.
<div id="site"> <header> <svg id="logo"></svg> <!-- insertion point 1 --> </header> <nav> <!-- insertion point 2 --> </nav> <main> <!-- insertion point 3 --> </main> <footer> © 2015 OpenTable, Inc. </footer> </div> <style> /* imagine CSS here */ </style>
Next, let’s think about how we might want to use the ot-site
component. And let’s set up some really basic sample content so that we can see what we’re accomplishing.
<ot-site> <ot-head> I render in head. </ot-head> <ot-menu> I render in menu. </ot-menu> <ot-body> I render in body. </ot-body> </ot-site>
Now, what we still need to do is:
- Pick the right Web Components features
- Replace our comments with something functional
- Match our sample content with the appropriate insertion point
- Combine and render the result
Shadow DOM
While Web Components are at their best when features from all the specifications are used together, as projects like Polymer show, it’s actually possible to implement exactly what we need using only the Shadow DOM, so that’s what we’ll focus on from here.
We’ve already looked at a user agent shadow DOM, but what is a shadow DOM exactly? One way to begin understanding a shadow DOM is by comparing it to a document fragment. Unlike a document fragment, a shadow DOM is a complete DOM with a root and it can scope styles. While you still have access to JavaScript’s global scope (so do use an IIFE), a shadow DOM can provide encapsulation, especially in the bundling sense. Also, using a shadow DOM does impact the way events behave. Since ot-site
won’t require any scripting beyond what’s required to make the shadow DOM itself, there’s more information about shadow DOM event model elsewhere if you need it.
Shadow Host
All shadow DOMs start with a shadow host, an element in the regular DOM (now often being called the light DOM) which plays host to the shadow DOM so that it is attached to the document. Being a shadow host is the destiny of ot-site
tag we invented above.
<ot-site> <ot-head> I render in head. </ot-head> <ot-menu> I render in menu. </ot-menu> <ot-body> I render in body. </ot-body> </ot-site>
If we place this as-is in an otherwise empty HTML document, the result will be:
Shadow Root
The next step is to make ot-site
a shadow host by creating a shadow root. The Shadow DOM specification defines a new createShadowRoot
method on all elements for that purpose.
var otSite = document.querySelector("ot-site"); otSite.createShadowRoot()
The DOM when inspected in developer tools will now show a shadow root “in” ot-site
and, as a shadow host, ot-site
now straddles the light and shadow worlds. One symptom of this condition is that all of our sample content doesn’t show up on the page anymore.
<ot-head> I render in head. </ot-head> <ot-menu> I render in menu. </ot-menu> <ot-body> I render in body. </ot-body>
While this may seem a little strange, it’s one step on the way to something very useful. Now that we have a shadow DOM, we can add things to it and the things we add to ot-site
‘s shadow DOM will show up.
Filling Shadow DOM
The things we’ll add are the same things in our boilerplate HTML from earlier—the header, logo, menu, body, footer, copyright, and styles—all of the things we want built into ot-site
.
otSite.shadowRoot.innerHTML = ` <div id="site"> <header> <svg id="logo"></svg> <!-- insertion point 1 --> </header> <nav> <!-- insertion point 2 --> </nav> <main> <!-- insertion point 3 --> </main> <footer> © 2015 OpenTable, Inc. </footer> </div> <style> /* imagine CSS here */ </style> `
Note: If those backticks (“) don’t look familiar, they’re an ECMAScript 6 feature called template strings. They’re much less noisy for HTML inlined in JavaScript because line breaks can be used and quotes don’t have to be escaped. These aren’t well-supported in browsers yet, so a tool like Babel is required to utilize them today.
Resulting DOM
The DOM, when inspected, now contains the boilerplate in the shadow DOM and the sample content in the light DOM. As a result, we will now be able to see the styled boilerplate of the ot-site
content scaffold.
<ot-site> #shadow-root <div id="site"> <header> <svg id="logo"></svg> <!-- insertion point 1 --> </header> <nav> <!-- insertion point 2 --> </nav> <main> <!-- insertion point 3 --> </main> <footer> © 2015 OpenTable, Inc. </footer> </div> <style> /* imagine CSS here */ </style> <ot-head> I render in head. </ot-head> <ot-menu> I render in menu. </ot-menu> <ot-body> I render in body. </ot-body> </ot-site>
Inserting Content
However, the sample content is still missing, instead of being inserted, because all we have are placeholder comments marking the insertion points. The Shadow DOM specification also defines content
, a tag that creates a content insertion point for user-authored markup.
<content></content>
For a situation like with ot-site
, when there are multiple insertion points in a single shadow DOM, there is an optional select
attribute. The value of the select
attribute is a CSS selector. Descendant elements of ot-site
from the light DOM which match the CSS selector will appear at the insertion point.
<content select="*"></content>
Let’s swap the placeholder comments with content
tags.
<div id="site"> <header> <svg id="logo"></svg> <content select="???"></content> </header> <nav> <content select="???"></content> </nav> <main> <content select="???"></content> </main> <footer> © 2015 OpenTable, Inc. </footer> </div> <style> /* imagine CSS here */ </style>
Note! This has been superseded by simpler alternative, slots. See the proposal to change content distribution, the slots proposal, and the webkit announcement by Ryosuke Niwa. It’s similar in functionality, but the syntax is not backwards compatible and a number of details differ.
Matching Content
In order to match each insertion point with the appropriate user-authored content, we’ll refer back to our sample content. Earlier, when we considered the way we might want to use ot-site
, we were actually choosing a declarative programming interface for inserting content into ot-site
.
<div id="site"> <header> <svg id="logo"></svg> <content select="ot-head"></content> </header> <nav> <content select="ot-menu"></content> </nav> <main> <content select="ot-body"></content> </main> <footer> © 2015 OpenTable, Inc. </footer> </div> <style> /* imagine CSS here */ </style>
In the above code any light DOM children of ot-site
of the type ot-head
will appear in the header
next to the logo. The same will be true for ot-menu
and ot-body
inside ot-site
.
As an aside, it isn’t strictly necessary to to register official custom elements for any of this to work, although it is an option with the Custom Elements specification. Doing so, however, adds a several dimensions to a Web Component that are powerful and essential for certain types of components. More on that another time.
Rendering Content
With all this done, we’re at the last step, when the resulting composition is rendered. This is part of using a shadow DOM that is worth dwelling on. The document only renders as if the light DOM and shadow DOM(s) are comboned.
Content Projection
The best way of describing what is actually happening may be what some are calling “content projection”. The user’s light DOM content is projected onto the insertion point. Imagine a movie being projected from a projector room onto the screen in a theater. Shadow DOM functionality renders content in some ways as if it were there, although it isn’t—remember the resulting DOM we inspected above?
One way to understand this concept is using styles to visualize it. Suppose we added a style in our light DOM head that made
ot-head
, ot-menu
, and ot-body
red.
ot-head, ot-menu, ot-body { color: red; }
Without a shadow DOM the text inside would of course show up red.
And with a shadow DOM the text inside those three tags will still show up—regardless (with a significant exception) of colors defined in the shadow DOM’s embedded stylesheet, no matter how specific they are.
The tags we’re using to earmark each piece of sample content don’t get moved to the shadow DOM, they remain in the light DOM and—so do their text contents. Naturally, they are affected by styles in the light DOM. But equally naturally, they are not affected by typical styles in the shadow DOM.
Similarly, suppose we add a rule to the light DOM that attempts to make the text of any footer
, especially a #site footer
, red.
footer, #site footer { color: red; }
I can re-use the same screenshot, because that footer
is in the shadow DOM. Light DOM styles don’t affect it, so the copyright text in our boilerplate will not change color.
There is an interactive demo of ot-site
with visuals for content projection on CodePen.
I implied there were exceptions to this, and there are. There is a set of new CSS selectors designed to work with shadow DOM and allow styles to be applied in other scopes. (Note: Apple has proposed a different way to style shadow DOM elements by allowing web component authors to define their own pseudo elements that I personally think is a great idea.)
One such selector is the ::content
pseudo-element, which represents elements distributed to content insertion points. We could use this to override the red.
header ::content ot-head, nav ::content ot-menu, main ::content ot-body { color: #FFF; }
Another among the new selectors is the ::shadow
pseudo-element, which represents the shadow root of a light DOM element. With this, we could apply the red color to the footer as we couldn’t above.
ot-site::shadow footer { color: red; }
Removing the red styles that illustrate content projection provides the result we originally aimed for: an encapsulated content scaffold component that allows user-authored content to be inserted in three locations.
There is an interactive demo of the final ot-site
on CodePen.
Note! The newer slot distribution model affects the selectors here, too. There’s also new progress and clarifications on styles and inheritance with a Shadow DOM. See CSS Scoping Level 1, CSS Inheritance Level 3, CSS Custom Properties/Variables Level 1, CSS @apply, and this Github discussion.
Conclusion
Kara Erickson and I gave a talk on implementing ot-site
as a Web Component, as an Angular 1.3.x directive, and as an Angular 2.0 directive at ng-conf 2015. Both a video and slides with speaker notes are available online.