Building web components from a loose framework of mixins
December 7, 2015
We think it’s generally necessary to use some sort of framework to develop web components, but that framework may not have to be monolithic in nature. Instead, the framework might be built entirely as mixins on top of a kernel that enables mixin composition. Rather than invoking a framework’s class constructor, one would simply compose the desired mixins together to create an instantiable web component.
We’ve been prototyping a completely mixin-oriented approach to component development in a project called core-component-mixins.
- This relies on the Composable facility as the kernel to compose mixins in JavaScript. An alternative mixin strategy could be used as long it retained the same general degree of expressiveness. It would be ideal if multiple web component frameworks could agree on a mixin architecture so that we could share some of these mixins. We’d be happy to use a different mixin strategy in order to collaborate with more people.
-
The repo’s /src folder shows a core set of component mixins for
template stamping, basic attribute marshaling, and Polymer-style automatic
node finding. For example, the TemplateStamping mixin will add a
createdCallback that creates a shadow root and clones into it the value of
the component’s template property:
import TemplateStamping from 'core-component-mixins/src/TemplateStamping';
Use of the TemplateStamping mixin takes care of details like shimming anyclass MyElement extends Composable.compose(HTMLElement, TemplateStamping) { get template() { return
<style> :host { font-weight: bold; } </style> Hello, world.
; } }<style>
elements found in the template when running under the Shadow DOM polyfill. -
That /src folder contains a sample ReactiveElement base class that pre-mixes
the three core mixins mentioned above to create a reasonable starting point
for custom elements. The above example becomes:
import ReactiveElement from 'core-component-mixins/src/ReactiveElement';
class MyElement extends ReactiveElement { get template() { return
<style> :host { font-weight: bold; } </style> Hello, world.
; } }Use of the ReactiveElement class is entirely optional — you could just as easily create your own base class using the same mixins.
- The /demo folder shows some examples of components created with this mixin-based framework. such as Hello World example.
- A demo of a hypothetical X-Tag implementation shows how a framework can use mixins to create its own custom element base class. In that demo, the hypothetical framework adds support for a mixin that provides X-Tag’s “events” sugar, but leaves out the mixin for automatic node finding. The point is that frameworks and apps can opt in to the component features they want.
- In this approach, web component class definition is generally kept separate from custom element registration. That is, there’s no required entry point like Polymer() to both create the class and register it in a single step. We personally feel that keeping those two steps separate makes each step clearer, but that’s a matter of taste. If you feel that combining those steps makes your code easier to write or read, it’s easy enough to accomplish that. The X-Tag demo shows how a framework could define an entry point for class definition and registration.
- The mixin architecture explicitly supports custom rules for composing specific properties. That’s intended for cases like the “properties” key in Polymer behaviors, where object values supplied by multiple mixins need to get merged together. The Composable kernel supports that, although none of the demos currently show off that feature.
Taken collectively, these core component mixins form the beginnings of a deliberately loose but useful framework for web component development. They’re still rudimentary, but they already provide much of what we need from a layer like polymer-micro. We think this strategy confers a number of advantages:
- This is closer to the metal. The only new thing here is the concept of a mixin. Everything else is part of the web platform. There’s no special class constructor required to perform black-box operations on a component. There’s nothing new to master (like React’s JSX or Polymer’s <dom-element>) that’s not already in the platform. There’s no sugaring provided out of the box — and that’s a good thing.
- Each mixin can focus on doing a single task really well. For example, the TemplateStamping mixin just creates a shadow root and stamps a template into it. The only real work it’s doing is to normalize the use of native vs polyfilled Shadow DOM — that is, the work you’d need to do anyway to work on all browsers today. Given the boilerplate nature of that task, it’s reasonable to share that code with a mixin like this. Once all the browsers support Shadow DOM v1 natively, this mixin could be simplified, or dropped entirely, without needing to rearchitect everything.
- You can stay as close to/far from the platform as you want. Most user interface frameworks take you far away from the platform in one giant step. Here you have fine-grained control over each step you take toward a higher level of abstraction. Each mixin takes you a tiny bit further away from the platform, and in exchange for the efficiency boost the mixin provides, you have to accept some trade-offs: performance, mystery, etc. That’s an unavoidable price for sharing code, but at least this way you can decide how much you want to pay.
- There’s a potential for cross-framework mixins. If multiple web component frameworks could agree on a mixin architecture, there’d at least be a chance we could share good solutions to common higher-level problems at the sub-component level. When Component Kitchen creates a mixin to support, say, accessibility in a list-like web component, it would be great if we could make that available to people developing list-like web components in other frameworks. While any framework could in theory adopt some other framework’s mixin format, mixins are usually intimately tied to a framework. Explicitly deciding to factor mixins into a separable concept may make cross-framework mixins more feasible.
It’s worth remembering that web components are, by their very nature, interoperable. If you decide to write a component using an approach like this, it’s still available to someone who’s using a different framework (Polymer, say). The reverse is also true. That means any team can pick the approach that works for them, while still sharing user interface elements at the component level.
As we’re experimenting with these mixin ideas in prototype form, we’re opportunistically trying some other technology choices at the same time:
- These mixins are written in ES6. As the polymer-micro blog post mentioned, we’re finding that ES6 makes certain things easy enough in JavaScript that we can use the DOM API directly, rather than relying on a framework for sugar. Transpiling with Babel feels like a fine temporary solution while waiting for native ES6 implementations in all browsers.
- While the core component mixins are written in ES6, they can still be used by plain ES5 apps. The Hello World (ES5) demo shows this in practice.
- The TemplateStamping mixin assumes use of the Shadow DOM polyfill if you want to support browsers that don’t yet support Shadow DOM. If the majority of the world’s web users have a Shadow DOM v1-capable browser by, say, the second half of 2016, we think businesses might accept using the polyfill to support the shrinking number of users with older browsers. To the extent using that polyfill has issues, those issues should diminish over time.
- We use JavaScript module imports as the dependency mechanism rather than HTML Imports. That lets us leverage tools like browserify for concatenation rather than Vulcanize. So far, that’s working okay. ES6 template strings let us easily embed HTML directly inside of JavaScript files, instead of putting JavaScript code inside of HTML files as we did with HTML Imports. Both packaging formats can work, but given the need for JavaScript modules anyway, it seems worthwhile for us to see what we can build with modules alone. One thing we miss: an equivalent of HTML Import's “document.currentScript” so that a module can load a resource from a path relative to the JavaScript source file.
- We’re trying out npm as the primary means of component distribution. We think that npm 3’s support for dependency flattening addresses much of the need for Bower. We think the combination of ES6 modules and npm may prove to be a better way to distribute components, so we’re trying that out with this prototype to see if we could make the switch to dropping Bower entirely. So far, this feels very good.
This mixin-based component framework isn’t done, but feels like it’s reached the point where it’s “good enough to criticize”. Please share your feedback at; @Component or +ComponentKitchen.