#jsist2014 @Web_Components @PascalPrecht

Componentize all the things!

Pascal Precht
Pascal Precht

Oh my gosh this talk on Web Components by @PascalPrecht blows my mind! #jsist2014

Web Components

Web Components are a collection of standards that change the way we build web applications.

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

HTML Templates

Inert chunks of clonable DOM that can be activated for later use.

What we did before...

Offscreen DOM

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>
  • We can clone the DOM
  • Nothing is rendered
  • Not inert, network request is made for the image
  • Styling is painful, all styles must be prefixed with #mytemplate

Overloading script

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>
  • Nothing rendered, <script> is display: none; by default
  • It's inert (yay!), JS isn't parsed because of its type
  • Security issues, run-time string of user-supplied data can easily lead to XSS vulnerabilities

HTML Templates to the rescue!

<template id="mytemplate">
  <img src="path/to/source.png" alt="great image">
  <div class="comment"></div>
</template>
  • Clonable DOM
  • Parsed, not rendered
  • Stylesheets/Images aren't loaded, media isn't played, scripts don't run

Activating a template

You can activate templates by cloning their content document fragment.

var t = document.querySelector('#mytemplate');

t.content.querySelector('img').src = 'logo.png';
document.body.appendChild(t.content.cloneNode(true));

Demo

A few gotchas to know about...

No prerendering

  • Templates can't be "precompiled" like you can with libs like handlebars
  • Assets, images, JS processing can't be preloaded

Nested templates behaviour

Activating outer template will not activate inner templates.

<template>
  <ul>
    <template>
      <li>Stuff</li>
    </template>
  </ul>
</template>

Reads

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

Shadow DOM

Shadow DOM is a beast!

Shadow DOM gives us DOM tree encapsulation and style boundaries.

The web platform offered only one built-in mechanism to isolate one chunk of code to another - <iframe>

What do you mean I must put each of my custom buttons in a separate iframe? What kind of insane are you?

<input type="time">

<details>Content goes here</details>
Merhaba millet! Bu konuşma Web Components hakkında!

Creating Shadow DOM

.createShadowRoot() creates a shadow root on a host element.

<button>Hello World!</button>
var host = document.querySelector('button');
var shadowRoot = host.createShadowRoot();
shadowRoot.textContent = 'Hello Jsist!';

Demo

It's just HTML

Use typical JS APIs to put nodes into your Shadow DOM (.innerHTML, .appendChild()...).

var host = document.querySelector('#host');
var shadowRoot = host.createShadowRoot();

var p = document.createElement('p');
p.innerHTML = 'This is HTML code';

/**
 * Both work
 */
shadowRoot.innerHTML = '<p>This is HTML code</p>';
shadowRoot.appendChild(p);

So that means we can use HTML Templates together with Shadow DOM?

YES!

Shadow DOM <3 Templates

Simply combine the things we just learned.

<template id="custom-template">
  <p>This is HTML Code</p>
</template>
var host = document.querySelector('#host');
var shadowRoot = host.createShadowRoot();
var t = document.querySelector('#custom-template');

shadowRoot.appendChild(t.content.cloneNode(true));

Great! But is there also a way to separate the content from presentation?

YES!

Insertion Points

<content></content> lets you create insertion points that cherry-pick content from the shadow host.

Let's create a special button

<template id="special-button-template">
  <style>
    .special-button {
      background: #bada55;
      color: red;
      padding: 1em;
      border-radius: 0.3em;
      font-size: 1em;
    }
  </style>
  <button class="special-button">Special Button!</button>
</template>
<span id="special-button"></span>
<span id="special-button">Special Button!</span>
<span id="special-button">Merhaba!</span>
<template id="special-button-template">
  <style>
    .special-button {
      background: #bada55;
      color: red;
      padding: 1em;
      border-radius: 0.3em;
      font-size: 1em;
    }
  </style>
  <button class="special-button">Special Button!</button>
</template>

Demo

<template id="special-button-template">
  <style>
    .special-button {
      background: #bada55;
      color: red;
      padding: 1em;
      border-radius: 0.3em;
      font-size: 1em;
    }
  </style>
  <button class="special-button"><content></content></button>
</template>

Demo

Insertions Points are to Web Components, what transclusion is to AngularJS (sort of).

Can we decide what content is projected and which not?

YES!

Cherry-picking content projection

select attribute uses CSS selectors to specify where children are projected.

<div id="host">
  <h1>My Title</h1>
  <h2>My Subtitle</h2>
  <div>...other content...</div>
</div>
<hgroup>
  <content select="h2"></content>
  <content select="h1:first-child"></content>
</hgroup>

Wow! Can we even select descendents for projection like select="table tr"?

NO!

Style Encapsulation

Shadow DOM comes with a shadow boundary that enables style encapsulation for free. Styles are scoped to shadow root

<div>
  <h3>Light DOM</h3>
</div>
<script>
var host = document.querySelector('div').createShadowRoot();
host.innerHTML = '<style>h3 { color: red; }</style>' +
                 '<h3>Shadow DOM</h3>';
</script>

Shadow DOM

Styling the host element

:host allows us to style the element hosting a shadow tree.

<button>My button</button>
<script>
var button = document.querySelector('button');
var host = button.createShadowRoot();
host.innerHTML = '<style>' +
                 ':host { text-transform: uppercase; }' +
                 '</style>' +
                 '<content></content>';
</script>

Moar host selectors

  • :host(selector) - Matches if host itself selector
  • :host(:hover/:active) - Know pseudo selectors

Context selectors

:host-context(selector) - Matches if host element or any of its ancestors match selector

<div class="theme-name">
  <x-foo></x-foo>
</div>
:host-context(.theme-name) {
  /* styles */
}

Styling Shadow DOM from outside

We have two new selectors to cross the shadow boundaries and style Shadow DOM from the outside world.

  • ::shadow - matches the shadow root itself
  • /deep/ - Crosses into any number of shadow trees

The ::shadow pseudo-element

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>
var host = document.querySelector('div');
var root = host.createShadowRoot();

root.innerHTML = '<span>Shadow DOM</span>' +
                 '<content></content>';
Shadow DOMLight DOM

The /deep/ combinator

Useful when having multiple levels of Shadow DOM (very common when working with Custom Elements.

x-tabs /deep/ x-panel {
  /* styles */
}

^ Select all x-panel elements that are descendents of x-tab anywhere in the tree.

Reads

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

Custom Elements

Custom Elements allow web developers to define new types of HTML elements.

Custom Elements Features

  • Define new HTML/DOM Elements
  • Extend existing elements
  • Bundle together custom functionality into a single tag

Registering new elements

document.registerElement() let's you create custom elements.

var MyElement = document.registerElement('my-element');
document.body.appendChild(new MyElement());

Or with an optional prototype

var MyElement = document.registerElement('my-element', {
  prototype: Object.create(HTMLElement.prototype)
});

Extend native/custom elements

Simply inherit prototype from HTML[type]Element and add extends property accordingly.

var MegaButton = document.registerElement('mega-button', {
  prototype: Object.create(HTMLButtonElement.prototype, {
    /* new APIs */
  }),
  extends: 'button'
});

Custom elements that inherit from native elements are called type extension custom elements.

Instantiate Custom Elements

Declarative:

Via DOM APIs:

var myElement = document.createElement('my-element');

Using new:

var myElement = new MyElement();
document.body.appendChild(myElement);

Lifecycle callback methods

  • createdCallback - instance of the element is created
  • attachedCallback - instance was inserted into the document
  • detachedCallback - instance was removed into the document
  • attributeChangedCallback - attribute was added, removed or updated
var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {
  prototype: proto
});

Alright, so can we add markup to our custom element?

SURE!

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {
  this.innerHTML = '<p>HTML goes here</p>';
};

var XFoo = document.registerElement('x-foo', {
  prototype: proto
});

Also works with Templates and Shadow DOM?

SURE!

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {
  var t = document.querySelector('#sdtemplate');
  var clone = t.content.cloneNode(true);
  this.createShadowRoot().appendChild(clone);
};

var XFoo = document.registerElement('x-foo', {
  prototype: proto
});

Reads

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

Four technologies

  • HTML Templates - Inert chunks of clonable DOM

  • Shadow DOM - Style & DOM encapsulation

  • Custom Elements - Define and use new DOM elements

  • HTML Imports - Include/Reuse other HTML documents

HTML Imports

#include for the web

Why Imports?

Different types of the web that can be loaded:

  • <script src> - For JavaScript
  • <link rel="stylesheet"> - For Stylesheets
  • <img>, <video>, <audio>...

So what about HTML itself?

Here are our options

  • <iframe> - super heavy, super restrictive, frustrating to script in/out
  • AJAX - xhr.responseType="document", wtf? We need JS to load HTML?
  • Hacks - embedded in strings, hidden as comments

HTML Imports to the rescue!

Use <link rel="import"> to import external HTML documents.

<link rel="import" href="path/to/import.html">

Turns this...






...into this.

<link rel="import" href="bootstrap.html">

Getting the content

var content = document.querySelector('link[rel="import"]').import

Cloning contents into main document

var link = document.querySelector('link[rel="import"]');
var content = link.import;
          
var el = content.querySelector('.widget');

document.body.appendChild(el);

Content is inert until cloned into the DOM!

Reads

Putting it all together...

Demo

So what about Polymer and X-Tag?

Both make the development of web components easier and better!

FACT!

Declaring elements with Polymer


  
  

Declaring elements with X-Tag

var frag = xtag.createFragment('

This is a foo-element.

'); xtag.register('foo-element', { lifecycle: { created: function () { this.appendChild(frag.cloneNode(true)); } } });

X-Tag doesn't use Shadow DOM by default.

Shadow DOM is optional in X-Tag

xtag.register('my-element', {
  lifecycle: {
    created: function () {
      var tpl = document.querySelector('#my-template');
      var shadow = this.createShadowRoot()
      shadow.appendChild(tpl.content.cloneNode(true));
    }
  }
});

Reads

Thank you.

Pascal Precht