Created
March 14, 2016 23:19
-
-
Save JanMiksovsky/e6063505af036740c955 to your computer and use it in GitHub Desktop.
Summary of Basic Web Components architecture, focusing on its use of mixins
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Basic Web Components architecture | |
================================= | |
The project seeks to provide a comprehensive set of solid, well-designed web | |
components that implement very common user interface patterns. | |
Goals: | |
* Usability excellence | |
* Work in a wide variety of situations (Gold Standard) | |
* As functionally complete as possible | |
* Provide good building blocks — meant to be combined/extended | |
* Appeal to a broad audience of developers | |
Boilerplate tolerance | |
===================== | |
How much repetitive code will you tolerate before creating an abstraction? | |
// Native event listener | |
constructor() { | |
super(); | |
this.addEventListener('tap', event => this._handleTap(event)); | |
} | |
// Polymer event listener | |
listeners: { | |
'tap': '_handleTap' | |
} | |
// Native event dispatch | |
this.dispatchEvent(new CustomEvent('kick', { detail: { kicked: true }})); | |
// Polymer event dispatch | |
this.fire('kick', { kicked: true }); | |
Our observations | |
================ | |
* ES2015 is actually pretty concise. | |
* A framework can be even more concise, but requires more specialized knowledge. | |
* Although native DOM and framework code both require substantial knowledge, | |
native knowledge can be applied broadly in any web app. | |
Conclusions: | |
* An open project trying to draw contributions from a large general audience is | |
better off accepting a higher tolerance for boilerplate code. | |
* We are willing to accept boilerplate 3-5 times longer than an abstraction that | |
might replace it. | |
* When we do have to introduce an abstraction, we prefer a imperative function | |
or a class that leaves things under developer control. We now prefer these to | |
black-box mechanisms that can invert/limit control. | |
Do we need HTML Imports? | |
======================== | |
* ES2015 template strings make embedding HTML relatively painless. | |
class MyElement extends HTMLElement { | |
constructor() { | |
let root = this.attachShadow({ mode: 'open' }); | |
root.innerHTML = ` | |
Hello, | |
<slot></slot>. | |
`; | |
} | |
} | |
* JavaScript import statements, currently processed with a transpiler, give us | |
modules and a way to manage dependencies. | |
* Setting aside HTML Imports has given us a much simpler toolchain based on | |
tools — Babel, browserify/webpack — with much broader support. | |
* One issue: module-relative loading of non-code resources like images. | |
Creating web components requires some degree of scaffolding | |
=========================================================== | |
* There is a vogue for speaking against frameworks. | |
* People say they want to write in "vanilla JavaScript". | |
* But in practice, writing robust web components requires some degree of reuse. | |
* We would like to find a low-impact way to share code across components that | |
entails as little framework as possible. | |
Shared web component features | |
============================= | |
Low-level features: | |
* Template stamping | |
* Marshalling attributes and properties | |
* Automatic element references (automatic node finding) | |
Mid-level features: | |
* ARIA support for lists, etc. | |
* Swipe gestures | |
* Keyboard navigation | |
* Keyboard prefix typing / AutoComplete | |
* Selection navigation | |
* Selection representation | |
JavaScript alone does not provide a sufficiently rich composition model. | |
Mixins can provide features like these, but we would like to avoid a | |
framework-specific mixin model. | |
A core mixin problem is resolving name conflicts | |
================================================ | |
The lack of a standard JavaScript mixin construct creates inherent ambiguity | |
when working with mixins. | |
let mixin1 = { foo() { ... } }; | |
let mixin2 = { foo() { ... } }; | |
let MyClass = FrameworkOfTheYear.createClass({ | |
foo() { ... } | |
mixins: [mixin1, mixin2] | |
}); | |
let instance = new MyClass(); | |
instance.foo(); // Does... what? | |
JavaScript already has a disambiguation mechanism: the prototype chain. | |
A functional approach to mixins | |
=============================== | |
Mixins can just be functions that extend the prototype chain: | |
let MyMixin = (base) => class MyMixin extends base { | |
// Mixin defines properties and methods here. | |
greet() { | |
return "Hello"; | |
} | |
}; | |
// Mixin application is just a function call. | |
let NewClass = MyMixin(MyBaseClass); | |
let obj = new NewClass(); | |
obj.greet(); // "Hello" | |
Conventions for mixin composition | |
================================= | |
* A mixin is responsible for calling base property/method implementations. | |
* We rely on boilerplate code to ensure composability rather than a class | |
factory or other wrapper to broker property and method calls. | |
let Mixin = (base) => class Mixin extends base { | |
// Mixin defines a greet method. | |
greet(...args) { | |
// If there's a greet() further up the prototype chain, invoke it. | |
if (super.greet) { super.greet(...args); } | |
// ... Do the mixin's work here ... | |
return "Hello"; | |
} | |
}; | |
* This pattern ensures a property/method call goes up the prototype chain. | |
* Feels like we are working with JavaScript, not against it. | |
Using mixins to create web components | |
===================================== | |
import ShadowTemplate from 'basic-component-mixins/src/ShadowTemplate'; | |
// Create a simple custom element that supports a template. | |
class GreetElement extends ShadowTemplate(HTMLElement) { | |
get template() { | |
return `Hello, <slot></slot>.`; | |
} | |
} | |
// Register the custom element with the browser. | |
document.registerElement('greet-element', GreetElement); | |
General-purpose Web component mixins | |
==================================== | |
* Marshall element attributes to component properties. | |
* Translate a click on a child element into a selection. | |
* Facilitate the application of a set of mixins. | |
* Let a component treat its content as items in a list. | |
* Allow a component to take its first child as a target. | |
* Translate direction (up/down, left/right) semantics into selection semantics. | |
* Access the nodes distributed to a component as a flattened array or string. | |
* Define the content of a component as its (flattened, distributed) children. | |
* Lets a component easily disable standard, optional styling. | |
* Allow a set of items in a list to be selectable. | |
* Let a component handle keyboard events. | |
* Translate directional keys (e.g., Up/Down) into direction semantics. | |
* Translate page keys (Page Up/Page Down) into selection semantics. | |
* Translate prefix typing into selection semantics. | |
* Wire up mutation observers to report changes in component content. | |
* Define open/close semantics. | |
* Treat the selected item in a list as the active item in ARIA terms. | |
* Apply standard text highlight colors to the selected item in a list. | |
* Scroll the component to keep the selected item in view. | |
* Lets a component easily access elements in its Shadow DOM subtree. | |
* Define template content that should be cloned into a Shadow DOM subtree. | |
* Translate left/right touch swipe gestures into selection semantics. | |
* Share keyboard handling with target element. | |
* Track and manage selection for a separate target element. | |
* Allow the selection to be updated on a timer. | |
* Translate trackpad swipes into direction semantics. | |
All of these can be used on their own, or in combination. | |
Some advantages of using mixins | |
=============================== | |
* Low conceptual overhead. | |
* Feels lighter weight than a framework. | |
* Complements ES2015 well. | |
* Can mix-and-match just the features you care about. | |
* Mixins can get applied in radically different contexts. | |
* Easy to unit test. | |
Rolling your own base classes | |
============================= | |
We create a base class as a set of mixins applied to HTMLElement: | |
// Some commonly-used mixins | |
let mixins = [ | |
Composable, | |
ShadowTemplate, | |
ShadowElementReferences, | |
AttributeMarshalling, | |
DistributedChildren | |
]; | |
// Apply all the mixins to HTMLElement. | |
let ElementBase = mixins.reduce((base, mixin) => mixin(base), HTMLElement); | |
// Create a new custom element. | |
class NewCustomElement extends ElementBase { ... } | |
* There is nothing special about the base class. | |
* Mixins handle responsibilities similar to both Polymer features and behaviors. | |
The mixin set above is roughly comparable to the features in polymer-micro. | |
Examples | |
======== | |
Subjective assessment | |
===================== | |
* Very little feels special here. | |
* Functional mixins let us share code across components. | |
* Using composition instead of inheritance is a win. | |
* Using ES2015 directly lets us capitalize on the most popular tools. | |
* Related: Switched to npm for component distribution, which is working fine. | |
* People who say "I do not want to use a framework" seem to like the approach. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment