Skip to content

Instantly share code, notes, and snippets.

@JanMiksovsky
Created March 14, 2016 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JanMiksovsky/e6063505af036740c955 to your computer and use it in GitHub Desktop.
Save JanMiksovsky/e6063505af036740c955 to your computer and use it in GitHub Desktop.
Summary of Basic Web Components architecture, focusing on its use of mixins
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