basic-component-mixins
Mixin library for creating web components in plain JavaScript (ES5 or ES6)
This library implements common web component features as JavaScript mixins. It
is designed for people who would like to create web components in plain
JavaScript while avoiding much of the boilerplate that comes up in component
creation. The mixins permit flexibility and a pay-as-you-go approach to
complexity and performance.
Design goals:
- Focus each mixin on solving a single, common component task.
Each mixin should be useful on its own, or in combination.
- Introduce as few new concepts as possible.
A developer who understands the DOM API should be able to work with these
mixins without having to substantially change the way they write code. They
shouldn't have to learn many proprietary concepts beyond those listed below,
chiefly the notion of defining a mixin as a function.
- Anticipate native browser support for ES6 and web components.
The architecture should be useful in an ES5 application today, but should
also feel correct in a future world in which native ES6 and web components
are everywhere.
All of the top-level Basic Web Components are constructed with these mixins. In
fact, by design, most of those components are little more than combinations of
mixins. That factoring allows you to create your own web components in the
likely event that your needs differ from those driving the design of the Basic
Web Components. You can use these mixins without using those components.
Mixin concepts
Mixins as functions
The mixins in this library all take the form of a function. Each function takes
a base class and returns a subclass defining the desired features:
let MyMixin = (base) => class MyMixin extends base {
// Mixin defines properties and methods here.
greet() {
return "Hello";
}
};
class MyBaseClass {}
let NewClass = MyMixin(MyBaseClass);
let obj = new NewClass();
obj.greet(); // "Hello"
Many JavaScript mixin implementations destructively modify a class prototype,
but mixins of the functional style shown above do not. Rather, functional mixins
extend the prototype chain and return a new class. Such functions have been
called "higher-order components", but we prefer the term "mixin" for brevity.
The mixins in this package take care to ensure that base class properties and
methods are not broken by the mixin. In particular, if a mixin wants to add a
new property or method, it also invokes the base class' property or method. To
do that consistently, these mixins follow standardized [Composition
Rules](Composition Rules.md). If you are interested in creating your own
component mixins, you may find it helpful to follow those guidelines to ensure
that your mixins can interoperate cleanly with the ones in this package.
A core virtue of a functional mixin is that you do not need to use any library
to apply it. This lets you use these mixins with any conventional means of
defining JavaScript classes — you don't have to invoke a proprietary class
factory, nor do you have to load a separate framework or runtime.
Because mixins define behavior through composition, you're not limited by the
constraints of a single-inheritance class hierarchy. That said, you can still
use a class hierarchy if you feel that's suitable for your application. For
example, you can compose a set of mixins to create a custom base class from
which your other classes derive. But the use of such a base class is not
dictated here.
Semantic mixin factoring
In a number of areas, this package factors high-level component services into
mixins that work together to deliver the overall service. This is done to
increase flexibility.
For example, this library includes three mixins that work in concert. When
applied to a custom element, these mixins take care of mapping presses on
keyboard arrows (Left/Right) into selection actions (select previous/next).
They each take care of a different piece of the problem:
- The Keyboard mixin wires up a single keydown listener on
the component that can be shared by multiple mixins. When the component has
the focus, a keypress will result in the invocation of a
keydown
method. By
default, that method does nothing. - The KeyboardDirection mixin maps keyboard
semantics to direction semantics. It defines a
keydown
method that maps
Left/Right arrow key presses into calls to methods goLeft
and goRight
,
respectively. By default, those methods do nothing. - The DirectionSelection mixin maps direction
semantics to selection semantics. It defines
goLeft
and goRight
methods
which respectively invoke methods selectPrevious
and selectNext
. Again, by
default, those methods do nothing.
If all three mixins are applied to a component, then when the user presses, say,
the Right arrow key, the following sequence happens:
(keyboard event) → keydown() → goRight() → selectNext()
Other mixins can map selection semantics to user-visible effects, such as
highlighting the selected item, ensure the selected item is in view, or do
something entirely new which you define.
Such factoring may initially feel overly complex, but permits a critical degree
of developer freedom. You might want to handle the keyboard a different way,
for example. Or you may want to create a component that handles arrow
keypresses for something other than selection, for example. Or you may want to
let the user manipulate the selection through other modalities, such as touch
gestures, mouse actions, speech commands, etc.
As one example of another mode of user input, the
SwipeDirection mixin maps touch gestures to goLeft
and goRight
method calls. It can therefore be used in combination with the
DirectionSelection mixin above, with the result that swipes will change the
selection:
(touch event) → goRight() → selectNext()
The SwipeDirection and KeyboardDirection mixins are compatible, and can be
applied to the same component. Users of that component will be able to change
the selection with both touch gestures and the keyboard.
This factoring allows components with radically different presentations to
nevertheless share a considerable amount of user interface logic. For example,
the basic-carousel and
basic-list-box components look very different, but
both make use of same mixins to support changing the selection with the
keyboard. In fact, nearly all of those components' behavior is defined through
shared mixins factored this way.
Using the mixins to create web components
Installation of the mixins package via npm
Add a dependencies
key for basic-component-mixins
in your project's
package.json file. Until native Shadow DOM support is available on all browsers
you want to support, you'll want to include the webcomponents.js
polyfill as well:
{
...
"dependencies": {
"basic-component-mixins": "^0.7",
"webcomponents.js": "^0.7.2"
},
}
Then issue an npm install
as usual.
A sample-component
project demonstrates the use of npm to depend on the basic-component-mixins
package. It shows the creation of a simple component in both ES6 and ES5.
ES6
Your ES6 code can reference the mixin as a function exported by the
corresponding file in this package's /src folder. You then apply the mixin to
the element base class you'd like to use. This can be HTMLElement
, or an
element class of your own creation.
As a very simple example, if you'd like to create a web component that puts the
word "Hello" before its tag contents, you can use the ShadowTemplate mixin. This
will look for a template
property on the component, attach a new Shadow DOM
subtree to the component, then copy the template into the shadow subtree.
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);
Compile this source with your favorite ES6 processor (e.g.,
Babel), then load the result into a page.
<html>
<head>
<script src="node_modules/webcomponents.js/webcomponents.js"></script>
<script src="greet-element.js"></script>
</head>
<body>
<!-- Hello, world. -->
<greet-element>world</greet-element>
</body>
</html>
ES5
This package's /dist folder contains a JavaScript file that defines all the
mixins as globals available via window.Basic
. For example, the ShadowTemplate
mixin shown above is available as window.Basic.ShadowTemplate
.
You can create your custom element class by hand:
// greet-element.js
var GreetElement = Basic.ShadowTemplate(HTMLElement);
GreetElement.prototype.template = 'Hello, <slot></slot>.';
document.registerElement('greet-element', GreetElement);
You can also use Composable mixin to create the class:
// greet-element.js
var GreetElement = Basic.Composable(HTMLElement).compose(
Basic.ShadowTemplate,
{
template: 'Hello, <slot></slot>.'
}
);
document.registerElement('greet-element', GreetElement);
Then load the script defining the element into your page:
<html>
<head>
<script src="node_modules/webcomponents.js/webcomponents.js"></script>
<script src="node_modules/basic-component-mixins/dist/basic-component-mixins.js"></script>
<script src="greet-element.js"></script>
</head>
<body>
<!-- Hello, world. -->
<greet-element>world</greet-element>
</body>
</html>
Web component mixins
The /src folder includes the complete set of mixins, each of which address some
common web component feature:
- AttributeMarshalling.
Marshall element attributes to component properties (and eventually vice
versa). This includes mapping hyphenated
foo-bar
attribute references to
camelCase fooBar
property names. - ClickSelection.
Translates a click on a child element into a selection.
- Collective.
Not a mixin itself, this class is used by the CollectiveMember mixin to track
the components that should be treated as a unit for keyboard purposes.
- Composable.
Facilitates the application of a set of mixins.
- composeTemplates.
Not a mixin, but a helper function for letting a component insert its template
inside a template defined by a base class.
- ContentAsItems.
Lets a component treat its content as items in a list.
- ContentFirstChildTarget.
Allows a component to take its first child as a target it wants to augment in
some way.
- DirectionToSelection.
Translates direction (up/down, left/right) semantics into selection semantics
(select previous/next).
- DistributedChildren.
Helpers to access the nodes distributed to a component as a flattened array
or string.
- DistributedChildrenAsContent.
Defines a component's content as its (flattened, distributed) children.
Typically used in conjunction with the DistributedChildren mixin.
- Generic.
Lets a component easily disable standard, optional styling.
- ItemsSelection.
Allows a set of items in a list to be selectable.
- Keyboard.
Lets a component handle keyboard events. Includes support for collective
keyboard handling.
- KeyboardDirection.
Translates directional keys (e.g., Up/Down) into direction semantics
(up/down).
- KeyboardPagedSelection.
Translates page keys (Page Up/Page Down) into selection semantics.
- KeyboardPrefixSelection.
Translates prefix typing into selection semantics. This allows, e.g., a list
box to allow selection by typing the start of the desired list item.
- ObserveContentChanges.
Wires up mutation observers to report any changes in a component's content
(direct children, or nodes distributed to slots).
- OpenClose.
Adds open/close semantics.
- SelectionAriaActive.
Treat the selected item in a list as the active item in ARIA accessibility
terms.
- SelectionHighlight.
Applies standard text highlight colors to the selected item in a list.
- SelectionInView.
Scrolls the component to keep the selected item in view.
- ShadowElementReferences.
Lets a component easily access elements in its Shadow DOM subtree.
- ShadowTemplate.
Makes it easy for a component to define template content that should be cloned
into a Shadow DOM subtree when the component is instantiated.
- SwipeToDirection.
Translates left/right touch swipe gestures into selection semantics.
- TargetInCollective.
Adds a component's target element (e.g., the component's first child) to
the set of elements collectively handling the keyboard.
- TargetSelection.
Allows a component to track and manage selection for a separate target
element.
- TimerSelection.
Allows the selection to be updated on a timer.
- TrackpadDirection.
Translates trackpad swipes or horizontal mouse wheel drags into direction
semantics.
Applying multiple mixins
Since web components often include many mixins, a helper mixin called Composable
provides syntactic sugar that allows multiple mixins to be applied in a single
call. Instead of defining an element like:
class MyElement extends Mixin1(Mixin2(Mixin3(Mixin4(HTMLElement)))) {
...
}
You can write:
class MyElement extends Composable(HTMLElement).compose(
Mixin1,
Mixin2,
Mixin3,
Mixin4
) {
...
}