Component
Future of HTML

Eric Bollens / eric@eb.io / ebollens [GitHub] / @ericbollens [Twitter]

Before We Start...

About This Presentation

The slides from this presentation
eb.io/p-web-components

Example code from this presentation
github.com/ebollens/p-web-components/tree/gh-pages/ex

This presentation is open source
github.com/ebollens/p-web-components

About Me

Chief Technology Officer

Formerly, Open Source Architect

Open source, modern web & IoT evangelist

The Original Vision for HTML

Originally, just text and links

Support added for images and media

Becomes a publishing and marketing medium

By the late 1990's, the web was no longer optional

From Information to Interaction

DotCom era bore a new generation of web tech

eCommerce
Social networks
Blogs, wikis and podcasts
Streaming services

Boom turned to bust, but "Web 2.0" remained

Rich Internet Applications
Web-oriented Architecture
Social Web

Keeping Up with the Changing Model

HTML suffers from its success

Divitis
XHTML, DHTML, ActiveX, Java
JavaScript Mega-Frameworks
Flash & Silverlight

HTML 5

Structural semantics
Interactive features

A larger feature set... but still a discrete one

Beyond Discrete Semantics

Semantics should be

Domain-applicable
Expressive
Encapsulated
Reusable
Composable
Extensible

Web Components

Custom Elements
define new HTML tags

HTML Templates
inject dynamic content into static markup structures

Shadow DOM
scope markup and styles in a separate DOM tree

HTML Imports
import HTML documents into other HTML documents

We'll Also Touch On

ES6 Modules & Module Loading
for keeping JavaScript namespaces sane

Polyfills
bringing the future to today

A Motivating Example

Data collection @ CloudCompli

Develop software for environmental compliance
Workflows driven by inspections & other data collection
Compliance isn't simple or accommodating
Need a simple definition language for complex forms

We need more than HTML forms offer out-of-the-box

Why Isn't HTML Enough?

HTML forms are flat
Hard to represent sets, sub-structures & complex data types

HTML forms are missing controls we need
Signatures, geocoding, graphical, etc.

HTML forms are weak on behavioral definitions
Conditional applicability, multi-group sets, etc.

A Comparison

The Old Way
Most forms over 1k LOC
Largest form over 3k LOC
Limited reusability
Mid-level engineer required

The New Way
Largest form less than 1k LOC
Control behaviors completely reusable
"Intern-ready"

Custom Elements

Basics

Register a custom element

var XFoo = document.registerElement('x-foo');

Use within the DOM

<x-foo></x-foo>

Instantiate the element

document.body.appendChild(new XFoo());

More About Registration

var XFoo = document.registerElement('x-foo');
var XFoo = document.registerElement('x-foo', { 
  prototype: Object.create(HTMLElement.prototype) 
});
var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.foo = function() { alert('foo() called'); };

XFooProto.bar = 5;

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

Extending an Element

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

Attributes, methods and callbacks inherited from  button

<button is="mega-button">
var megaButton = document.createElement('button', 'mega-button');
[is="mega-button"] { font-size: 2em; }

For Convinience, Setters & Getters

var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.foo = function() { alert('foo() called'); };
XFooProto.bar = 5;
var XFoo = document.registerElement('x-foo', {
  prototype: XFooProto
});
var XFoo = document.registerElement('x-foo', { 
  prototype: Object.create(HTMLElement.prototype, { 
    foo: { 
      value: function() { alert('foo() called'); } 
    },
    bar: { 
      get: function() { return 5; } 
    }
  })
});

Custom Element Lifecycle

createdCallback
when instance of element is created

attachedCallback
when an instance is inserted into the document

detachedCallback
when an instance is removed from the document

attributeChangedCallback(attrName, oldVal, newVal)
when an attribute is added, removed or updated

Putting Custom Elements to Work

Latitude Input: Custom Callback

<input is="type-lat" name="latitude">
var initializeInputTypeLat = function(){
  var self = this;
  self.type = 'number';
  self.min = -90;
  self.max = 90;
  self.step = 'any';
  navigator.geolocation.getCurrentPosition(function(position){
    self.value = position.coords.latitude;
  });
}
document.registerElement('type-lat', {
  extends: 'input',
  prototype: Object.create(HTMLInputElement.prototype, {
    attachedCallback: {
      value: initializeInputTypeLat
    }
  })
});

Nested Data

<form is="hierarchical-form" action="/report" method="POST">
    <input type="text" name="title">
    <fieldset is="control-group" data-name="location">
        <legend>Location</legend>
        <input is="type-lat" name="latitude">
        <input is="type-lng" name="longitude">
    </fieldset>
    <input type="submit">
</form>
{
    "title": "Example",
    "location": {
        "latitude": "34.068077", 
        "longitude": "-118.442542"
    }
}

Control Group

<fieldset is="control-group" data-name="location">
    <legend>Location</legend>
    <input is="type-lat" name="latitude">
    <input is="type-lng" name="longitude">
</fieldset>
group.value == {
    "latitude": "34.068077", 
    "longitude": "-118.442542"
}

control-group as a meta-field
It's value should be an object of sub-fields
It's name should be used for form data or in a parent group

Control Group: Value Getter

document.registerElement('control-group', {
  extends: 'fieldset',
  prototype: Object.create(HTMLFieldSetElement.prototype, {
    value: {
      get: getValueForControlGroup
    }
  })
});
var getValueForControlGroup = function(){
    var values = {};
    $(this).find(processableSelector)
           .not($(this).find(exclusionSelector))
           .each(function(){
        var name = $(this).attr('name');
        if(name && this.value !== undefined){
            values[name] = this.value;
        }
    });
    return values;
}

Control Group: Name

data-name attribute for fieldset name

document.registerElement('control-group', {
  extends: 'fieldset',
  prototype: Object.create(HTMLFieldSetElement.prototype, {
    value: {
      get: getValueForControlGroup
    },
    name: {
      get: function(){
        return $(this).attr('data-name');
      },
      set: function(value){
        $(this).attr('data-name', value);
      }
    }
  })
});

Control Group: Value Setter

document.registerElement('control-group', {
  extends: 'fieldset',
  prototype: Object.create(HTMLFieldSetElement.prototype, {
    value: {
      get: getValueForControlGroup,
      set: setValueForControlGroup
    },
    /* .. */
  })
});
var setValueForControlGroup = function(){
    $(this).find(processableSelector)
       .not($(this).find(exclusionSelector))
       .each(function(){
            var name = this.name;
            if(value[name])
                this.value = value[name]
    });
}

Form: Values

form.value == {
    "title": "Example",
    "location": {
        "latitude": "34.068077", 
        "longitude": "-118.442542"
}   }
form.value = {
    "some-group": {
        "some-field": "some value"
}   }
document.registerElement('hierarchical-form', {
    extends: 'form',
    prototype: Object.create(HTMLFormElement.prototype, {
        value: {
            get: getValueForControlGroup,
            set: setValueForControlGroup
        }
    })
});

Form: Submission

<form is="hierarchical-form" action="server/submit.php" method="POST"
      data-success="server/success.php" data-error="server/error.php">
document.registerElement('hierarchical-form', {
    extends: 'form',
    prototype: Object.create(HTMLFormElement.prototype, {
        attachedCallback: {
            value: function(){ overrideFormSubmitHandler(this); }
}   })  });
var overrideFormSubmitHandler = function(form){
    $(form).submit(function(e){ 
        e.preventDefault();
        $.ajax({ url: $(form).attr('action'),
                 method: $(form).attr('method'),
                 data: JSON.stringify(form.value),
                 contentType: 'application/json; charset=utf-8',
                 success: function(){ 
                     window.location = $(form).attr('data-success'); },
                 error: function(){ 
                     window.location = $(form).attr('data-error'); }
})  })  };

Getting Organized

Quickly Becoming a Mess

(function(){
    
    var processableFields = [
            'input', 
            'textarea', 
            'select', 
            'fieldset[is="control-group"]'
        ],
        processableSelector = processableFields.join(', '),
        exclusionSelector = processableFields
            .map(function(f){ 
                return 'fieldset[is="control-group"] '+f; 
            }).join(', ');
    
    var HierarchicalGroupValueProperties = {
        value: {
            get: function(){
                
                var values = {}

                $(this).find(processableSelector)
                       .not($(this).find(exclusionSelector))
                       .each(function(){
                    var name = this.name;
                    if(name && this.value !== undefined){
                        values[name] = this.value;
                    }
                });

                return values;
            },
            set: function(value){
                
                $(this).find(processableSelector)
                       .not($(this).find(exclusionSelector))
                       .each(function(){
                    var name = this.name;
                    if(value[name])
                        this.value = value[name]
                });
                
            }
        }
    }
    
    window.HierarchicalFormElement = document.registerElement(
            'hierarchical-form', {
        extends: 'form',
        prototype: Object.create(HTMLFormElement.prototype, 
                $.extend({}, HierarchicalGroupValueProperties, {
            attachedCallback: {
                value: function(){
                    var form = this;
                    $(this).submit(function(e){
                        e.preventDefault();
                        $.ajax({
                            url: $(form).attr('action') 
                                    ? $(form).attr('action') 
                                    : '#',
                            method: $(form).attr('method') 
                                    ? $(form).attr('method') 
                                    : 'GET',
                            data: JSON.stringify(form.value),
                            contentType: 
                                'application/json; charset=utf-8',
                            success: function(){ 
                                if($(form).attr('data-success'))
                                    window.location = 
                                        $(form).attr('data-success'); 
                            },
                            error: function(){ 
                                console.log(arguments)
                                if($(form).attr('data-error'))
                                    window.location = 
                                        $(form).attr('data-error'); 
                            }
                        })
                    })
                }
            }
        }))
    });

    window.ControlGroupElement = document.registerElement(
            'control-group', {
        extends: 'fieldset',
        prototype: Object.create(HTMLFieldSetElement.prototype, 
                $.extend({}, HierarchicalGroupValueProperties, {
            name: {
                get: function(){
                    return $(this).attr('data-name');
                },
                set: function(value){
                    $(this).attr('data-name', value);
                }
            }
        }))
    });

    window.InputTypeLatElement = document.registerElement(
            'type-lat', {
        extends: 'input',
        prototype: Object.create(HTMLInputElement.prototype, {
            attachedCallback: {
                value: function(){
                    var self = this;
                    self.type = 'number';
                    self.min = -90;
                    self.max = 90;
                    self.step = 'any';
                    navigator.geolocation.getCurrentPosition(
                        function(position){
                            self.value = position.coords.latitude;
                        });
                }
            }
        })
    });

    window.InputTypeLngElement = document.registerElement(
            'type-lng', {
        extends: 'input',
        prototype: Object.create(HTMLInputElement.prototype, {
            attachedCallback: {
                value: function(){
                    var self = this;
                    self.type = 'number';
                    self.min = -180;
                    self.max = 180;
                    self.step = 'any';
                    navigator.geolocation.getCurrentPosition(
                        function(position){
                            self.value = position.coords.longitude;
                        });
                }
            }
        })
    });
    
})()

Partial Loading

Great for resources

<script src>
<link rel="stylesheet" href>
<img> , <video>   and  <img>
<embed>   and  <object>

But historically bad for HTML

AJAX
<iframe>
Hacks (overloading <script>, hiding in comments, etc.)

HTML Imports

<link rel='import' href='elements/fieldset_control-group.html'>
<script type='text/javascript' src="../libraries/jquery.js"></script>
<script type='text/javascript'>
document.registerElement('control-group', {
    /* .. */
});
</script>

Support for recursion

<link rel='import' href='elements.html'>
<link rel='import' href='elements/form_hierarchical-form.html'>
<link rel='import' href='elements/fieldset_control-group.html'>
<link rel='import' href='elements/input_type-lat.html'>
<link rel='import' href='elements/input_type-lng.html'>

Namespace Chaos

Multiple elements may need the same routines

var HierarchicalGroupValue = (function(){

    var processableFields = [
            'input', 
            'textarea', 
            'select', 
            'fieldset[is="control-group"]'
        ],
        processableSelector = processableFields.join(', '),
        exclusionSelector = processableFields
            .map(function(f){ 
                return 'fieldset[is="control-group"] '+f; 
            }).join(', ');

    return {
        get: function(){

            var values = {}

            $(this).find(processableSelector)
                   .not($(this).find(exclusionSelector))
                   .each(function(){
                var name = this.name;
                if(name && this.value !== undefined){
                    values[name] = this.value;
                }
            });

            return values;
        },
        set: function(){

            $(this).find(processableSelector)
                   .not($(this).find(exclusionSelector))
                   .each(function(){
                var name = this.name;
                if(value[name])
                    this.value = value[name]
            });

        }
    };

})();

ES6 Modules

var processableFields = ['input', 'textarea', 'select', 'fieldset[is="control-group"]'],
    processableSelector = processableFields.join(', '),
    exclusionSelector = processableFields.map(function(f){ 
            return 'fieldset[is="control-group"] '+f; 
        }).join(', ');
export function get(){ /* .. */ };
export function set(value){ /* .. */ };
import { get } from 'hierarchicalGroupValue';
get.call(element)
import * from 'hierarchicalGroupValue';
hierarchicalGroupValue.get.call(element)
import * from 'hierarchicalGroupValue';
document.registerElement('control-group', {
    extends: 'form',
    prototype: Object.create(HTMLFieldSetElement.prototype, {
        value: hierarchicalGroupValue,
        attachedCallback: { /* .. */ }
    })
});

ES6 Module Loader

System.import('helpers/hierarchical-group-value').then(function(hgv){
    document.registerElement('hierarchical-form', {
        extends: 'form',
        prototype: Object.create(HTMLFormElement.prototype, {
            value: hgv,
            /* .. */
        })
    });
});

Multiple ES6 Modules

Promise.all([
    'helpers/hierarchical-group-value', 'helpers/data-name'
].map(function(m){ return System.import(m) })).then(function(m) {
    var hierarchicalGroupValue = m[0],
        dataName = m[1];
    document.registerElement('control-group', {
        extends: 'fieldset',
        prototype: Object.create(HTMLFieldSetElement.prototype, {
            value: hierarchicalGroupValue,
            name: dataName
        })
    });
});

Templates & Shadow DOM

Multiple Group Control

Multiple sets of the same group of controls
and the ability to add more sets

<fieldset is='multi-group-control' data-name='issues'>
    <legend>Issues</legend>
    <fieldset is="control-group">
        <input type='text' name='identifier'>
        <textarea name='description'></textarea>
    </fieldset>
    <fieldset is="control-group">
        <input type='text' name='identifier'>
        <textarea name='description'></textarea>
    </fieldset>
    <!-- .. -->
    <button>+</button>
</fieldset>

Scaffolding the Control

Let's implement a Web Component for this where...

you define a template for the group

<fieldset is='multi-group-control' data-name='issues'>
    <legend>Issues</legend>
    <template>
        <input type='text' name='identifier'>
        <textarea name='description'></textarea>
    </template>
</fieldset>

the value is an array of the group values

[
    { "name": "..", "description": ".." },
    { "name": "..", "description": ".." }
]

Template Element

Inert until activated
Markup is hidden and doesn't render until activated

No side-effects
Scripts don't run and media isn't retrieved until activated

Not in DOM tree
Not selectable by usual DOM methods
May be placed anywhere in head, body or frameset

Using Template for Delayed Activation

<button onclick="example()">Use me</button>
<div id="container"></div>
<template> 
  <div>Template used: <span>0</span></div> 
  <script>alert('Foobar!')</script> 
</template>
function example() { 
  var content = document.querySelector('template').content,
      span = content.querySelector('span');

  span.textContent = parseInt(span.textContent) + 1;

  document.querySelector('#container')
          .appendChild(document.importNode(content, true));
}

Generating the Meta-Control

<fieldset is='multi-group-control' data-name='issues'>
    <legend>Issues</legend>
    <template>
        <input type='text' name='identifier'>
        <textarea name='description'></textarea>
    </template>
</fieldset>
attachedCallback: {
    value: function(){
        var control = this;
        this.template = $(this).children('template').get()[0];
        this.container = document.createElement('div');
        $(this).append(this.container);
        this.$button = $('<button>').attr('type', 'button')
                            .text('+')
                            .click(function(){
                                control.addControlGroup();
                             })
                            .appendTo(this);
    }
}

Generating a Control Group

<fieldset is='multi-group-control' data-name='issues'>
    <legend>Issues</legend>
    <template>
        <input type='text' name='identifier'>
        <textarea name='description'></textarea>
    </template>
</fieldset>
addControlGroup: {
    value: function(value){
        var clone = document.importNode(this.template.content, true),
            $newGroup = $('<fieldset is="control-group">');

        $newGroup.append(clone);
        $(this.container).append($newGroup);

        if(value !== undefined)
            $newGroup.get()[0].value = value;
    }
}

The Meta-Group's Getter & Setter

[
    { "name": "..", "description": ".." },
    { "name": "..", "description": ".." }
]
get: function(){
    var groups = [];
    $(this.container).children().each(function(){
        groups.push(this.value);
    });
    return groups;
}
set: function(values){
    values.forEach(function(value){
        this.addControlGroup(value);
    }, this);
}

Leaky Implementation Details

What about namespace clashes?

If we use a class in our widget, external definitions might leak in

When building and using widgets, we want to...

Encapsulate implementation details
Isolate from exterior styles and behaviors

We need to isolate the widget's DOM tree

Shadow DOM

this.container = document.createElement('div');

$(this).append(this.container);
var shadowContainer = document.createElement('div'),
    shadow = shadowContainer.createShadowRoot();

this.container = shadow;

$(this).append(shadowContainer);
<div>
    #shadow-root
        <fieldset is="control-group"><!-- .. --></fieldset>
        <!-- .. -->
</div>

A Lot More Than This

Styling shadow DOM trees

:host(:hover) { /* .. */ }
:host-context(.my-ctx) { /* .. */ }
[is="multi-group-control"] > ::shadow > [is="control-group"] {/* .. */}

Projecting content

<div is="name-badge">
  <span class="f">Eric</span>
  <span class="l">Bollens</span>
</div>
<template> 
  Hi! My name is
  <br><content select=".l"></content>, <content select=".f"></content>
</template>

Use with Caution!

Shadow DOM & template polyfills have to wrap...
Document, Window, Element, XMLHttpRequest, Node, NodeList, DOMTokenList, FormData, WebGLRenderingContext, Selection, and another 25 or so elements...

Oh, and functions like...
createElement, createEvent, getElementById, getSelection, querySelector, querySelectorAll, and another 15 or so core ones...

We love Web Components at CloudCompli, but...
we do not yet use template and Shadow DOM

Experiences from the Real World

This Stuff Isn't Easy

3 rewrites and 100 hours on our form components
But estimated savings producing forms over 10x that

Edge cases and race conditions
Event model and asynchronous loading get messy fast

Performance can be tough
You're in the DOM node initialization process

Browser bugs/inconsistencies and limited debugging
The browsers aren't doing this all right yet either

Browser Support, Polyfills and Failings

You are on the bleeding edge
But browsers are fixing bugs and shipping more

Polyfills do pretty good with most of this
But pieces of it are definitely not for the feint of heart

Web Components Polyfill
github.com/WebComponents/webcomponentsjs

ES6 Modules
github.com/ModuleLoader/es6-module-loader
github.com/google/traceur-compiler

Great Promise for the Future

We've only scratched the surface
So many more features and yet unforeseen outcomes

Makes HTML future-proof
Allows us to define our own domain-specific elements

Start using Web Components today
webcomponents.org
customelements.io
component.kitchen

Thank You

Any Questions?

Eric Bollens / eric@eb.io / ebollens [GitHub] / @ericbollens [Twitter]