Monday, September 17, 2012

Working with CSS regions and shadow DOM

CSS regions and shadow DOM

When we set out to develop CSS Regions we knew that most innovative applications of the technology would come from creative integrations with other web standards. Shadow DOM is one such example of a web standard just itching to be experimented with.

Shadow DOM is the enabling technology for Web Components, a W3C proposal which helps create self-contained units of functionality for web pages. These units are portable across pages because they’re built with strict encapsulation of DOM elements, styles and scripting provided by Shadow DOM. You can learn more by watching Christian Cantrell’s short video introduction to Shadow DOM and Web Components.

Shadow DOM in a nutshell

Shadow DOM provides the means to encapsulate DOM nodes to strictly control tampering by scripts and styles defined outside their boundaries. This allows developers to build complex components with bullet-proof style and behavior behind a single DOM node, similar to how the HTML5 video tag works.

Dimitri Glazkov, the author of the proposal, describes this in more detail on his blogpost about Shadow DOM.

It turns out that there’s a use case for CSS Regions that Shadow DOM is a perfect candidate for.

The CSS Regions Workflow

With CSS Regions you collect content from one or more elements and associate it to a logical structure called a named flow. Then, you use the named flow to point the collected content to render in other containers. Those containers become regions and they can be chained together with content flowing from one to the other.

CSS regions and shadow DOM

If this all sounds strange, maybe the CSS Regions samples can help you understand the concepts.

One way of doing this is by having empty placeholder elements in the document, waiting to become regions. But those elements have no semantic value; they’re just presentation helpers.

Developments such as CSS Pagination Templates show that using empty placeholders isn’t the only way to render content in regions. But, for the sake of experimentation, let’s tackle this limitation. Enter Shadow DOM.

The Use Case

With Shadow DOM we create as many placeholder elements as required under a single element. The catch is that we keep them outside the main DOM, nested under what's called a ShadowRoot, invisible to scripts defined outside this boundary.

Working with Shadow DOM

At the time of this writing Shadow DOM is a developing standard. Parts of the proposal are implemented with prefixes in WebKit and in Google Chrome behind runtime flags. In the spirit of the open web, the text of this article uses the unprefixed property names for Shadow DOM functionality. In the spirit of developer sanity, the code samples use Webkit prefixes where necessary at this point in time.

ShadowRoot Encapsulation

A good metaphor for Shadow DOM is a document within a document. A new Shadow DOM tree may be created under any DOM element with the ShadowRoot constructor. It takes a single DOM node argument. The DOM node acts as a host for the Shadow DOM root and it defines its boundaries. The original content of the host element is replaced by the content of the shadow root.

<div id="host"></div>

<script type="text/javascript">
  var host = document.querySelector('#host')
  var root = new WebKitShadowRoot(host)
</script>

Working under the ShadowRoot we may create other elements, attach event handlers and styles just as we would on a normal node.

var paragraph = document.createElement('p')
paragraph.textContent = 'in the shadow'

// The paragraph belongs to the shadow root
root.appendChild(paragraph)

By design, queries for elements made from outside the shadow root’s boundaries do not match the elements inside. This ensures encapsulation. The ShadowRoot implements its own query methods that match only against its child nodes: root.getElementById(), root.querySelector(), etc.

var elements = document.querySelectorAll('p') // 0 nodes
var elements = root.querySelectorAll('p') // 1 node

Stylesheets and Shadow DOM

By default, styles defined outside the shadow root will not be applied on its child elements. This also ensures encapsulation to protect the elements within.

<style type="text/css">
  p {
    color: green;
  }
</style>

<script type="text/javascript">
    var p = root.querySelector('p')
    window.getComputedStyle(p, null).color // rgb(0, 0, 0) black, not green
</script>

According to the Shadow DOM proposal there is a way to allow stylesheets defined outside the shadow root to apply on elements nested under it.

The boolean applyAuthorStyles flag on the shadow root will toggle this behavior.

// Apply stylesheets defined outside the shadow root
root.applyAuthorStyles = true

CSS Regions and Shadow DOM

By this point we have all the necessary components to meet the use case. A quick reminder about it: generate regions as placeholder elements under a ShadowRoot so as not to pollute the main DOM with non-semantic empty elements.

CSS Regions prerequisites

First, we need to collect the content of a source element into a named flow. We’ll call it myFlow. The element and its content will still be part of the main document and accesible via the DOM. The content, however, is not rendered. This is expected behavior.

We define a CSS class, .region, which, when applied to an element, will render content from the myFlow named flow into that element.

<style type="text/css">
  article{
    -webkit-flow-into: myFlow;
  }
  .region{
    -webkit-flow-from: myFlow;
    width: 150px;
    height: 300px;
  }
</style>
<article>
  <p>Lorem ipsum dolor...</p>
</article>>

Creating Regions in a Shadow DOM

To render the content from the myFlow named flow we need to create some regions that pull content from it. We’ll create those under the shadow root.

<div id="host"></div>

<script type="text/javascript">
  var host = document.querySelector('#host')
  var root = new WebKitShadowRoot(host)

  // Apply stylesheets defined outside the shadow root
  root.applyAuthorStyles = true

  // Create an empty div and make it a region
  var region = document.createElement('div')
  region.className = 'region'

  // Add the region to the shadow root
  root.appendChild(region)
</script>

The key part here is setting the applyAuthorStyles flag to boolean true on the ShadowRoot. This intentionally circumvents the Shadow DOM encapsulation. It allows the content defined outside, in the article tag to be rendered through CSS Regions in elements defined under the shadow root.

More regions can be created as necessary to render the whole content. This can be done by leveraging the CSS Regions JavaScript API, but that’s an experiment for another blog post.

Taken further, this idea is laying the groundwork for defining article templates with Shadow DOM. This approach helps achieve a clear separation between the semantic markup of the content from the auxiliary markup necessary to do the layout.

Next steps

At this point, both CSS Regions and Shadow DOM are under development and constantly evolving. A good way to test out these features together is to use Google Chrome Canary browser. You’ll need to type chrome://flags in the browser’s address bar then follow the instructions to enable the CSS Regions and the Shadow DOM flags.

Demo

Assuming you’re running an appropriate browser, you can see a demo of CSS Regions and Shadow DOM on GitHub. Be sure to take a peek at the source code, as well.