Eric Bollens / eric@eb.io / ebollens [GitHub] / @ericbollens [Twitter]
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
Chief Technology Officer
Formerly,
Open Source Architect
Open source, modern web & IoT evangelist
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
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
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
Semantics should be
Domain-applicable
Expressive
Encapsulated
Reusable
Composable
Extensible
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
ES6 Modules & Module Loading
for keeping JavaScript namespaces sane
Polyfills
bringing the future to today
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
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.
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"
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());
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
});
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; }
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; }
}
})
});
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
<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
}
})
});
<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"
}
}
<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
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;
}
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);
}
}
})
});
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.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 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'); }
}) }) };
(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;
});
}
}
})
});
})()
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.)
<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'>
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]
});
}
};
})();
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: { /* .. */ }
})
});
System.import('helpers/hierarchical-group-value').then(function(hgv){
document.registerElement('hierarchical-form', {
extends: 'form',
prototype: Object.create(HTMLFormElement.prototype, {
value: hgv,
/* .. */
})
});
});
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
})
});
});
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>
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": ".." }
]
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
<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));
}
<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);
}
}
<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;
}
}
[
{ "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);
}
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
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>
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>
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
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
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
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
Eric Bollens / eric@eb.io / ebollens [GitHub] / @ericbollens [Twitter]