The goal of this document is to describe how Ember could adopt semantics similar to web components and MDV. It relies on the HTMLBars templating engine, which allows Ember to directly control how templates are parsed and converted into DOM.
It builds on the work of Rafael Weinstein and the Polymer team, an attempts to harmonize that work with Ember's architecture.
Ember's scope is much wider than components, and is mostly focused on application architecture and URL-driven design. Today, we need a system to manage the lifecycle of data-binding and custom views, so we include such a system alongside our architectural tools.
Once the web provides its own tools for managing components, and eventually data bindings, Ember will embrace them and transition away from our Ember-specific solution. This document is how we get from here to there, continuing to build ambitious and stable applications in the meantime.
With platform-provided web components, all custom functionality is implemented on HTMLElement
s or subclasses of HTMLElement
s.
In Ember, custom functionality is implemented on view objects that manage the lifecycle of HTMLElements
. Eventually, once it is possible to extend elements in all browser we support, the responsibilities of Ember Views will be implemented on elements.
For example, if there is a custom element <app-pane>
:
<app-pane></app-pane>
In web components, when this element is inserted into the page, the readyCallback
method will be called on the element:
class AppPane extends HTMLElement {
readyCallback() {
// this method is called when an app-pane is added to the DOM
}
}
document.register('app-pane', AppPane);
In Ember, when this element is inserted into the page, the didInsertElement
method will be called on the element's view:
App.Pane = Ember.View.extend({
didInsertElement: function() {
// this method is called when an app-pane is added to the DOM
}
});
Ember.register('app-pane', App.Pane);
MDV, a Google extension to web components, also specifies a generic mechanism for doing data-bindings on elements:
<template>
<div title="{{title}}"></div>
</template>
The MDV system will call bind
on the <div>
when it instantiates the template:
divElement.bind('title', context, 'title');
The default implementation of bind
sets up an observer on context object, and updates the attribute whenever the property path changes.
Because the MDV system delegates the binding responsibility to the element, custom elements can implement their own binding behavior:
class SpecialInput extends HTMLElement {
bind(attribute, context, path) {
if (attribute !== 'value') { return super(attribute, context, path); }
// implement two-way value binding, a la <input type="text">
}
}
document.register('special-input', SpecialInput);
Ember will use a similar approach, but delegate the bind
to the Ember.View
that represents the element.
For built-in elements, like <div>
, Ember will supply a default implementation of bind
that uses the same infrastructure as bindAttr
uses today.
This means that custom elements, implemented using Ember.View
s, can override bind
in the same way as MDV envisions that custom elements will work.
App.SpecialInput = Ember.View.extend({
bind: function(attribute, context, path) {
if (attribute !== 'value') { return this._super(attribute, context, path); }
// implement two-way value binding, a la <input type="text">
}
});
For elements like <input>
, Ember will automatically use an appropriate view (such as Ember.TextField
) if particular attributes are bound. For example, an <input type="text" value="{{firstName}}">
would automatically instantiate and use an Ember.TextField
with a value binding.
Ember.register
allows the registration to be restricted to elements with particular attributes and particular bindings.
Ember.register('input', App.TextField, { type: 'text', bound: 'value' });
In this case, the App.TextField
implementation will only apply to input
elements with type="text"
and with the value
attribute specified using a binding.
You can think of App.TextField
as an Ember implementation of a built-in element. This serves both to implement behavior that will eventually be built-in (the value
binding on <input>
elements), and behavior that could be added to the <input>
element once HTML Elements are more well-defined.
Ember.View
classes allow us to act as if HTMLElement
s were configurable and extensible, even in today's browser that do not have direct support for these extensions.
In web components, the browser is responsible for the lifecycle of custom elements, and the instantiation of templates.
For example, if you consider the following template:
<template id="person">
<input type="text" value="{{firstName}}">
<p>{{firstName}}</p>
<ember-button action="{{submit}}">Submit</ember-button>
</template>
You can instantiate the template by providing it with a context:
var personTemplate = document.find('#person');
var person = {
firstName: "Yehuda",
submit: function() {
// implement code to save the person
}
};
var frag = post.instantiate(person);
When using Web Components with MDV, the browser will perform the following steps:
- Instantiate a new
<input>
tag withtype="text"
- Call inputElement.bind('value', person, 'firstName'). By default, this will:
- set the
value
to the"Yehuda"
- register an observer on
person
that updatesvalue
when thefirstName
property changes - register an
input
event handler on the input field that updatesperson.firstName
- set the
- Instantiate a
<p>
tag with a single text node - Call
textNode.bind(person, 'firstName')
. By default, this will:- Set the text node's
textContent
to the value ofperson.firstName
. - register an observer on
person
that updatestextContent
whenperson.firstName
changes.
- Set the text node's
- Instantiate an
EmberButton
(assumingdocument.register('ember-button', EmberButton
) - Call `emberButton.bind('action', person, 'submit');
- This will have element-specified behavior
- Once the element is inserted in the page, trigger the element's
readyCallback
In this case, the web browser is responsible for managing the entire lifecycle of element instantiation and data binding. It delegates its binding mechanism to elements, which gives custom elements the opportunity to perform whatever binding behavior they wish.
With HTMLBars, Ember takes responsibility for element instantiation. Because of this, it can emulate the flow of Web Components + MDV, using Ember View objects instead of custom elements.
Consider this equivalent template:
<script type="text/x-handlebars" id="person">
<input type="text" value="{{firstName}}">
<p>{{firstName}}</p>
<ember-button action="{{submit}}">Submit</ember-button>
</script>
Similarly, you can instantiate this template:
var personTemplate = Ember.TEMPLATES.person;
var person = {
firstName: "Yehuda",
submit: function() {
// implement code to save the person
}
};
var frag = post(person);
The HTMLBars system will perform the following steps (which are quite similar to the speculative MDV API):
- Instantiate a new
<input>
tag withtype="text"
- Instantiate an
Ember.TextField
, and associate it with the<input>
- Call textField.bind('value', person, 'firstName'). By default, this will:
- set the
value
to the"Yehuda"
- register an observer on
person
that updatesvalue
when thefirstName
property changes - register an
input
event handler on the input field that updatesperson.firstName
- set the
- Instantiate a
<p>
tag with a single text node - Instantiate an
Ember.SimpleBinding
and associate it with the text node. - Call
simpleBinding.bind(person, 'firstName')
. By default, this will:- Set the text node's
textContent
to the value ofperson.firstName
. - register an observer on
person
that updatestextContent
whenperson.firstName
changes.
- Set the text node's
- Instantiate a
<div>
- Instantiate an
EmberButton
(assumingEmber.register('ember-button', EmberButton
) and associate it with the<div>
- Call `emberButton.bind('action', person, 'submit');
- This will have view-specified behavior
- Once the element is inserted into the page, trigger the view's
didInsertElement
Because we cannot directly control the lifecycle of built-in nodes (like <input>
or text nodes), or reliably subclass elements, we use Ember objects to provide custom behavior and data binding.
It's important to note that HTMLBars converts templates into direct DOM calls, so it is able to emulate the future behavior of the browser without having to rely on the browser's unreliable parser or scan the DOM after the fact. This gives us high-fidelity emulation of the parser semantics of Web Components and MDV.
The goal of these semantics are to make it possible to emulate the future of the platform as envisioned by Polymer, but without trying to fully emulate subclass-ability of HTMLElement.
There are several reasons for this.
That kind of emulation is extremely invasive, and likely to have bugs and performance problems. In general, Ember does not rely on making such invasive changes, and is conservative even about making polyfills of ES5 features mandatory. This has proven necessary to avoid conflicts with other third party libraries that rely on their own buggy versions of these polyfills.
Perhaps more importantly, adopting the current semantics of HTMLElement#bind
and TextNode#bind
means that evolving changes to those semantics will break the Ember API. If we update the polyfill, we'll break existing apps, and if we don't, we won't be able to replace the polyfill with the real thing.
In contrast, we could keep APIs on Ember.View stable even when the DOM APIs evolve. Because it will likely be some time before these features stabilize and are available in all supported browsers, this strategy allows us to implement the important semantics of Web Components and MDV while maintaining API stability for users of Ember.
In the (very) long term, the goal of this strategy is to merge any remaining Ember.View
semantics with HTMLElement
. Ideally, we would be able to use <template>
and hand over lifecycle responsibility to the browser, and supply an Ember.View
subclass of HTMLElement
to ease upgrading and to handle any remaining responsibilities not handled by Web Components.
Ember templates should support elements implemented in Polymer.
Because the Polymer polyfill (and the feature as implemented in Chrome) handles the lifecycle custom elements defined using document.register
, Ember doesn't need to do anything special for Polymer custom elements to work in Ember templates.
In contrast with the custom element system, which is a further-along specification, the MDV system is still undergoing churn. We will need to figure out the precise semantics of custom elements integrating with Ember's data binding system.
A couple of options:
- Ember implements
Object.observe
semantics on its objects, and doesn't do Ember data binding on elements managed by Polymer - Ember supports interop with Polymer custom elements, but manages data binding itself. This would mean that Polymer custom elements would still have
Ember.View
objects to manage bindings, as if they were native elements as described above.
Both of these options represent transitional strategies. In the long-term, all custom elements will be web components, and all data binding will be managed by MDV. If custom binding is desired, a custom implementation of bind
on the subclass of HTMLElement
will be sufficient.
As I noted above, this strategy is different from the transitional strategy employed by Polymer, which is to attempt a full-fidelity polyfill of the web component system plus MDV.
This requires overriding nearly every DOM API to properly implement Shadow DOM, and requires implementing APIs like bind
on native elements.
This is a perfectly reasonable strategy, especially if the goal is to discover how the precise proposed API will feel in practice.
It has two important caveats.
First, such an invasive API will have bugs and performance problems that we are unwilling to accept on behalf of Ember's users. Also, because it patches native objects, it also only works on extremely new browsers, is not supportable in IE9, and does not currently work reliably even in IE10. In May 2013, Ember is not prepared to move to such an aggressive support matrix.
Second, because the APIs are still likely to change, apps that use them directly will break as the specifications evolve. By using a semantically similar strategy with a layer of indirection, we can keep the Ember API more stable even when the native API changes.
We are choosing a different transitional strategy that differs from Polymer's strategy, because our medium-term requirements are different, but our long-term goals are aligned.