To view the demos in this presentation...

  1. Use Chrome Canary
  2. Enable yourself:
    • Enable experimental WebKit features in about:flags
    • Enable experimental JavaScript in about:flags
    • Enable Show Shadow DOM in DevTools

You don't have Shadow DOM enabled

You don't have Object.observe()

You don't have MutationObservers

Who is this guy?

Web components?

Demo

<gangnam-style></gangnam-style>

( hover over the bar )

Demo

<my-megabutton>Mega button</my-megabutton>

Mega button

Embedded widgets

Reusable libraries / frameworks

We're going "Back to the Future"

A tectonic shift for web development

Important new constructs for building complex web apps

"Declarative Renaissance"

Once upon a time...

HTML5 had to define it's identity

Defining Web Components

key players

  • <template> - Scaffold/Blueprint
    • inert chunks of clonable DOM. Can be activated for later use (e.g MDV)
  • Custom elements - Toolbelt
    • create new HTML elements - expand HTML's existing vocabulary.
    • extend existing DOM objects with new imperative APIs
  • Shadow DOM - Mortar/glue
    • building blocks for encapsulation & boundaries inside of DOM

Defining Web Components

supporting cast

  • Style encapsulation
  • Knowledge into app's state
    • DOM changes: MutationObserver
    • Model changes: Object.observe()
  • CSS variables, calc()

A collection new capabilities in the browser

Get

ready!

Templates

scaffold / blueprint

Templates...

not
a new concept

The templates of today

Method #1: "offscreen" DOM using [hidden] or display:none

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>
  1. We're working directly w/ DOM.
  2. Resources are still loaded (e.g. that <img>)
  3. Other side-effects like URLs not being fully composed yet.
  4. Style and theming is painful.
    • embedding page must scope all its CSS to #mytemplate.
    • no guarantees on future naming collisions.

Today The templates of today

Method #2: manipulating markup as string. Overload <script>:

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>
  1. encourages run-time string parsing (via .innerHTML)
    • may include user-supplied data → XSS attacks.

Examples: handlebars.js, John Resig's micro-template script

Tomorrow Templating of the near future

<template>

Contains inert markup intended to be used later:

<template id="mytemplate">
  <img src="">
  <div class="comment"></div>
</template>
  1. We're working directly w/ DOM again.
  2. Parsed, not rendered
    • <script>s don't run, images aren't loaded, media doesn't play, etc.

Templating of the near future

  • Appending inert DOM to a node makes it go "live":
var t = document.querySelector('#mytemplate');
t.content.querySelector('img').src = 'http://...';
document.body.appendChild(t.content.cloneNode(true));
.content provides access to the <template>'s guts:

interface HTMLTemplateElement : HTMLElement {
  attribute DocumentFragment content;
}

Templates

baked into the web platform...

$$$

Shadow DOM

mortar / glue

Encapsulation...

not
a new concept

Encapsulation

Fundamental foundation of OOP

Separates code you wrote from the code that will consume it

We don't have it on the web!

Well, sort of:

<iframe>

<iframe> are heavy and restrictive

Shadow DOM

Is complex! Let's cover the basics:

  • Concept of Shadow DOM
  • Creating Shadow DOM
  • Insertion Points
  • Styling
    • encapsulated styles & shadow boundaries
    • styling hooks: custom pseudo elements
    • @host at-rule

Turns out...

  • DOM nodes can already "host" hidden DOM.
  • It can't be accessed traversing the DOM.


So...browser vendor's have been holding out on us!

Shadow DOM

exposes the same internals browser vendors have been using to implement their native controls, to us

Attaching Shadow DOM to a host

Shadow tree is rendered instead

Creating Shadow DOM

<div id="host">
  <h1>My Title</h1>
  <h2>My Subtitle</h2>
  <div>...other content...</div>
</div>
var host = document.querySelector('#host');
var shadow = host.createShadowRoot();
shadow.innerHTML = '<h2>Yo, you got replaced!</h2>' +
                   '<div>by my awesome content</div>';
// host.shadowRoot;

My Title

My Subtitle

...other content...

So....
unstyled markup is not sexy

Style encapsulation

<style>s defined in ShadowRoot are scoped.

var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<style>h2 { color: red; }</style>' + 
                   '<h2>Yo, you got replaced!</h2>' + 
                   '<div>by my awesome content</div>';

My Title

My Subtitle

...other content...

Protection from pesky outside styles

Author's styles don't cross shadow boundary by default, but have full control.

var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<style>h2 { color: red; }</style><h2>Yo, you got replaced!</h2>' +
                   '<div>by my awesome content</div>';
// shadow.resetStyleInheritance = true; // click me
// shadow.applyAuthorStyles = true; // click me

My Title

My Subtitle

...other content...
  • .resetStyleInheritance
    • false - (default) inheritable CSS properties continue to inherit.
    • true - resets properties to initial at the shadow boundary.
  • .applyAuthorStyles
    • true to allow author's styles to "bleed" across shadow boundary. Default is false.

Styling the host element (@host at-rule)

  • @host selects the shadow host element.
  • Allows reacting to different states:
<style>
@host {
  /* Gotcha: higher specificity than any selector,
     lower specificity than declarations from a <style> attribute. */
  * {
    opacity: 0.2;
    transition: opacity 400ms ease-in-out;
  }
  *:hover {
    opacity: 1;
  }
}
</style>

My Title

My Subtitle

...other content...

Custom pseudo elements

input[type=range].custom {
  -webkit-appearance: none;
  background-color: red;
}
input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

List of pseudo elements: WebKit, FF

Style hooks with custom pseudo elements

Widget author can designate certain elements be styleable by outsiders.

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
document.querySelector('#host').createShadowRoot().innerHTML = '<div>' +
  '<div pseudo="x-slider-thumb"></div>' + 
'</div>';
</script>
  • Note: name needs to be prefixed with "x-"
    • string name creates association with element in shadow tree
  • Can't access these elements from outside JS, but can style them!

Style hooks using CSS Variables

theming

Widget author includes variable placeholders:

button {
  color: var(button-text-color);
  font: var(button-font);
}

Widget embedder applies styles to the element:

#host {
  var-button-text-color: green;
  var-button-font: "Comic Sans MS", "Comic Sans", cursive;
}

DEMO

Remember our host node

<div id="host">
  <h1>My Title</h1>
  <h2>My Subtitle</h2>
  <div>...other content...</div>
</div>

that guy rendered as:

<div id="host">
  #shadow-root
    <style>h2 {color: red;}</style>
    <h2>Yo, you got replaced!</h2>
    <div>by my awesome content</div>
</div>

...everything was replaced when we attached the shadow DOM

Shadow DOM insertion points

funnels for host's children

Shadow DOM insertion points

funnels for host's children

  • <content> elements are insertion points.
  • select attribute uses CSS selectors to specify where children are funneled.
  • content.getDistributedNodes() returns list of element distributed in the insertion point.
<div id="host">
  <h1>My Title</h1>
  <h2>My Subtitle</h2>
  <div>...other content...</div>
</div>
<style>
  h2 {color: red;}
</style>
<hgroup>
  <content select="h2"></content>
  <div>You got enhanced</div>
  <content select="h1:first-child"></content>
</hgroup>
<content select="*"></content>

My Title

My Subtitle

...other content...

Insertion Points

allow us to define a declarative API

Encapsulation / Boundaries / Re-usability

baked into the web platform...

Cha-

ching!

Observing Changes

Mutation Observers / Object.observe()

Mutation Observers

watch for changes in the DOM

  • Defined in DOM4 ( Yep. We're on DOM4! )
  • Observers, not listeners
    • triggered by DOM changes rather than events (e.g. oninput, click)
  • Callback triggered at the end of DOM modifications.
    • provided a list of all changes (MutationRecords)
  • Replacement for MutationEvent performance / stability bottlenecks.
  • Availability: Chrome, Safari, FF
  • Who? JS Framework authors, extension developers

Example

observe child node insertion/deletions

var observer = new MutationObserver(function(mutations, observer) {
  mutations.forEach(function(record) {
    for (var i = 0, node; node = record.addedNodes[i]; i++) {
      console.log(node);
    }
  });
});

observer.observe(el, {
  childList: true,      // include childNode insertion/removals
  //subtree: true,        // observe the subtree root at el
  //characterData: true, // include textContent changes
  //attribute: true      // include changes to attributes within the subtree
});

// observer.disconnect() // Stop observations

Mutation Observers vs. Mutation Events

MutationEvent

  • Deprecated in DOM Events spec
  • Adding listeners degrades app performance by 1.5-7x!
    • slow because of event propagation and synchronous nature
    • fire too often: every single change
    • removing listener(s) doesn't reverse the damage
  • Inconsistencies with browser implementations.

Don't use Mutation Events!

Comparison Example

Inserting a DOM node

// MutationEvent
document.addEventListener('DOMNodeInserted', function(e) {
  console.log(e.target);
}, false);
// MutationObserver
var observer = new MutationObserver(function(mutations, observer) {
  mutations.forEach(function(record) {
    for (var i = 0, node; node = record.addedNodes[i]; i++) {
      console.log(node);
    }
  });
}).observe(document, {childList: true});

DEMO

Object.observe()

watch changes to JS objects

Object.observe : JS objects :: MutationObserver : DOM

function observeChanges(changes) {
  console.log('== Callback ==');
  changes.forEach(function(change) {
    console.log('What Changed?', change.name);
    console.log('How did it change?', change.type);
    console.log('What was the old value?', change.oldValue );
    console.log('What is the present value?', change.object[change.name]);
  });
}

var o = {};
Object.observe(o, observeChanges);
// Object.unobserve(o, observeChanges); // Stop watching.
  • Get notified when a property is added, deleted, or its value changes.
  • Get notified when properties are reconfigured (e.g. Object.freeze)
  • Landing in Chrome Canary. Turn on Experimental JavaScript APIs in about:flags.

See Bocoup's writeup.

Benefit: improved data binding

The AngularJS team experimented with a prototype build of Chromium:

20-40x faster!

Soon End game: Model Driven Views (MDV)

<div id="example">
<ul>
  <template iterate>
    <li>{{ name }}
      <ul><template iterate="skills"><li>...</li></template></ul>
    </li>
  </template>
</ul>
</div>
<script>
document.querySelector('#example').model = [
  {name: 'Sally', skills: ['carpentry']}, 
  {name: 'Helen', skills: ['weaving', 'omnipotence']}
];
</script>

Play with it today: code.google.com/p/mdv/

Today Templating in AngularJS

A (close-ish) taste of the future:

<div ng-controller="AppController">
  <ul>
    <li ng-repeat="person in model">{{ person.name }}
    <ul><li ng-repeat="skill in person.skills">{{skill}}</li></ul>
  </ul>
</div>
<script>
function AppController($scope) {
  $scope.model = [{name: 'Sally', skills: ['carpentry']},
                  {name: 'Helen', skills: ['weaving', 'omnipotence']}];
}
</script>
  1. {{ person.name }}
    • {{skill}}

Observing changes to DOM and JS

baked into the web platform...

$$$

Custom Elements

Putting it all together

Today Creating a basic tab control (YUI 3)

<div id="demo">
  <ul>
    <li><a href="#foo">foo</a></li>
    <li><a href="#bar">bar</a></li>
  </ul>
  <div>
    <div id="foo"><p>foo content</p></div>
    <div id="bar"><p>bar content</p></div>
  </div>
</div>
YUI().use('tabview', function(Y) {
  var tabview = new Y.TabView({srcNode: '#demo'});
  tabview.render();
});

But...there's nothing "basic" about it!

Insanity!!!

Do we have all the pieces to build components?

  1. Templates (<template>)
  2. Shadow DOM (@host, custom pseudo elements, styling hooks)
  3. Mutation Observers, Object.observe()

WE DO :)

Creating custom elements

Define a declarative "API" using insertion points:

<element name="my-tabs">
  <template>
    <style>...</style>
    <content select="hgroup:first-child"></content>
  </template>
</element>

Include and use it:

<link rel="import" href="my-tabs.html">
<my-tabs>
  <hgroup>
    <h2>Title</h2>
    ...
  </hgroup>
</my-tabs>

Define an API

Define an imperative API:

<element name="my-tabs" constructor="TabsController">
  <template>...</template>
  <script>
    TabsController.prototype = {
      doSomething: function() { ... }
    };
  </script>
</element>

Declared constructor goes on global scope:

<link rel="import" href="my-tabs.html">
<script>
var tabs = new TabsController();
tabs.addEventListener('click', function(e) { e.target.doSomething(); });
document.body.appendChild(tabs);
</script>

Or, extend existing [custom] elements

<element name="my-megabutton" extends="button" constructor="MegaButton">
  <template>
    <button><content></content></button>
  </template>
  <script>
    MegaButton.prototype = {
      megaClick: function(e) {
        play('moo.mp3');
      }
    };
  </script>
</element>

Use it:

<link rel="import" href="my-megabutton.html">
<my-megabutton>Mega button</my-megabutton>

Demo: Mega Button

<my-megabutton>Mega button</my-megabutton>

Mega button

Demo: Meme Generator

<my-meme src="images/beaches.jpg">
  <h1 contenteditable>Stay classy</h1>
  <h2 contenteditable>Web!</h2>
</my-meme>

DEMO

Example: tab component

Web Components

<my-tab>
  <h2>One</h2>
  <section>
    Code to instantiate this component:
    <pre></pre>
  </section>
  <h2>Two is bigger than three</h2>
  <section>
    Second panel hotness
  </section>
</my-tab>

AngularJS

<tabs ng-app="tabs">
  <pane title="One">
    Code to instantiate this component:
    <pre></pre>
  </pane>
  <pane title="Two is bigger than three">
    Second panel hotness.
  </pane>
</tabs>

DEMO

Defining / extending HTML elements

baked into the web platform...

Love

it!

Don't forget to enable yourself

Resources

Web Components / Custom Elements

Shadow DOM

Mutation Observers / Object.observe

Fine.