Skip to content

Instantly share code, notes, and snippets.

@wycats
Created May 20, 2013 15:51
Show Gist options
  • Star 84 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save wycats/9144666b0c606d1838be to your computer and use it in GitHub Desktop.
Save wycats/9144666b0c606d1838be to your computer and use it in GitHub Desktop.
Ember and Web Components

Ember and Web Components

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.

Elements

With platform-provided web components, all custom functionality is implemented on HTMLElements or subclasses of HTMLElements.

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);

Ember

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);

Binding

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

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.Views, 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 HTMLElements were configurable and extensible, even in today's browser that do not have direct support for these extensions.

Lifecycle

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 with type="text"
  • Call inputElement.bind('value', person, 'firstName'). By default, this will:
    • set the value to the "Yehuda"
    • register an observer on person that updates value when the firstName property changes
    • register an input event handler on the input field that updates person.firstName
  • 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 of person.firstName.
    • register an observer on person that updates textContent when person.firstName changes.
  • Instantiate an EmberButton (assuming document.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.

Ember

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 with type="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 updates value when the firstName property changes
    • register an input event handler on the input field that updates person.firstName
  • 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 of person.firstName.
    • register an observer on person that updates textContent when person.firstName changes.
  • Instantiate a <div>
  • Instantiate an EmberButton (assuming Ember.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.

Long Term Strategy

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.

Interop with Polymer Elements

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.

Transitional Strategies

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment