The Transition to Ember 2.0 in Detail
As we approach the eve of the release of Ember 1.13-beta, it's a good time to talk about the transition plan for those of us who have Ember 1.11 and 1.12 applications.
The high level:
- Ember 1.13-beta.1 will be released early this week. It will come with the new Glimmer engine and a series of new deprecations, preparing for cruft removal in Ember 2.0.
- Ember 1.13 will ship with support for angle bracket components, which also serve as the opt-in for one-way data flow and the entirety of the other React-inspired programming model improvements.
- We will continue to fix regressions caused by the Glimmer engine throughout the 1.13 beta cycle.
- We plan to release Ember 2.0-beta.1 on June 12, as announced at EmberConf.
- We will release a point release of Ember 1.13 (likely 1.13.1) together with the release of Ember 2.0. This release will continue to fix regressions caused by the Glimmer engine, and help smooth the transition to Ember 2.0.
- We will likely release additional point releases of Ember 1.13 to help address unexpected difficulties in the transition to Ember 2.0. For the most part, this will likely include new deprecations with light backporting of features needed to complete a transition away from deprecated features in 1.13.
The Glimmer engine represents the third phase of major performance work
on Ember in the past year (preceded by metal-views
in 1.8 and HTMLBars
in 1.10). Now that it's landed, we're going to move into the next phase
of major performance improvements. Expect to see more about that once
Ember 2.0 has shipped.
Dealing With Glimmer Regressions
We landed Glimmer in Canary, and given that it's a completely rewritten rendering engine, we're quite happy with the results so far.
That said, we expect to continue seeing compatibility fallout, especially in heavily-used private APIs, over the next several months.
As a result, we plan to continue releasing point releases in the 1.13 series as we learn about additional incompatibilities. We will continue to do this after Ember 2.0, to try to make sure that everyone who wants to upgrade to Ember 1.13 and remove deprecations (as a precursor to an Ember 2.0 upgrade) can do so.
Add-On Compatibility and Private APIs
Ember 1.x add-ons quite often use private APIs. This was necessary for many of the most ambitious add-ons (like Liquid Fire), and these add-ons were a boon for Ember users.
While Glimmer cannot maintain compatibility for every private API used by add-ons, we are committed to helping existing add-ons find new approaches that work post-Glimmer, ideally in the form of public APIs.
We know that many Ember 1.x apps (including apps by members of the core team) will not be able to upgrade to Ember 1.13 and Glimmer until popular existing add-ons can support 1.13. We don't plan to put the 1.13 series to bed until people with 1.12 apps who are trying to upgrade to deprecation-free 1.13 in earnest can do so successfully.
The Roll Out of New Features
Over the past six months, we've talked about a lot of new features that we plan to work on during this time-frame.
Since the 1.8.0 release (October 2014), we landed a large chunk of the features we started to talk about as precursors to 2.0.
In Ember 1.13, we will land the last chunk of 2.0 precursor features, including the Glimmer rendering engine and React-style data flow.
The Glimmer rendering engine is fully backwards compatible with the Ember 1.12 public API, and we continue to do work to ensure compatibility with real-world apps and addons that are using popular private APIs.
The new data flow model, including one-way data flow by default, separation of attributes from component state, opt-in mutable bindings, and callback-style actions are all opt-in through the use of another big new feature: angle-bracket components.
We do not plan to remove curly-brace components (with their legacy semantics) in Ember 2.0, so you can take your time upgrading your components to the new semantics and still upgrade to Ember 2.0. In the sections that follow, I will lay out the most important changes and talk about the transition path.
2.0 Precursor Features Already Landed By 1.12
- elimination of metamorph tags landed in Ember 1.8.0
- metal-views landed in Ember 1.8.0, providing full support for SVG documents, including components in SVG.
- the HTMLBars engine landed in 1.10, which prepared the codebase for Glimmer
- block params and chained else in templates landed in 1.10
- services landed in 1.10
- curly attributes / elimination of
bind-attr
(<a href="{{src}}">
), the first big fruits of HTMLBars, landed in 1.11 - the dynamic {{component}} helper landed in 1.11
- instance initializers, a precursor to FastBoot, landed in 1.12
Ember 1.13 and the Glimmer Engine
The Glimmer engine is shorthand for a whole bunch of new features. While most of the public discussion has focused on performance, it also integrates many of the programming model improvements pioneered by React.
One-Way Values By Default
First of all, starting in Ember 2.0, template bindings are one-way by default. Because we know that it will take some time to refactor your applications to explicitly opt-in to two-way bindings as needed, we have decided to make this new behavior a consequence of opting in to new angle-bracket components.
<my-component title= />
At the moment, we have not yet decided when precisely to drop two-way bindings by default, and whether to continue to include the support in a plugin once we have dropped it. The specifics will depend largely on how difficult the upgrade to angle-bracket components proves to be.
We expect angle-bracket components to land as part of Ember 1.13 on June 12.
Fast Re-Render
In Ember 1.12, calling rerender()
on a component is an extremely
expensive operation, and blows away all of the existing DOM (together
with its internal state, such as selection, cursor, focus, scroll
position and more).
In Ember 1.13, thanks to the Glimmer engine, you can safely invoke
rerender()
and it will only update the parts of the template that have
actually changed.
This allows you to replace an entire data structure with a totally new
POJO, rerender
the component, and get highly performant updates that
preserve the DOM.
As in React, if you modify a property on the component that is used in
the template ("state" in React), the component will be rerendered()
,
so the most common way to trigger a (fast) re-render is to set a
property on the component that is used by the component's template.
Notably, this model is quite similar to the binding model used in Ember 1.12, with some small changes that eliminate the possibility of creating complex graphs of two-way bindings by accident.
New Lifecycle Hooks
Instead of having to register observers on "bindings" and try to reflect the changes into your DOM, which can be fairly error-prone and confusing, Ember 1.13 introduces a series of new lifecycle hooks that execute whenever a component's attributes change.
Attribute changes can happen either via observation, such as when a
service has changed, or through the data-down/actions-up data flow
pattern enabled by component.rerender()
and component.set()
.
In Ember 1.12, you might write something like this:
Component.extend({
didInsertElement() {
this.$().button({
text: this.get('value'),
disabled: this.get('disabled')
});
},
valueDidChange: observer('value', function() {
this.$().option('text', this.get('value'))
}),
disabledDidChange: observer('disabled', function() {
this.$().option('disabled', this.get('disabled'))
})
})
One problem with this code is that the observer can fire at any time, and it executes both when the value is changed from inside the component and when it is changed from the outside.
As we'll see in the next section, conflating changes coming from the inside with changes coming from the outside can make it difficult to reason about the data flow, and makes it easy for code in your component to accidentally trigger observers that were intended for changes coming from the outside.
Starting with Ember 1.13, you will be able to express the same concept more clearly:
Component.extend({
didInsertElement() {
this.$().button({
text: this.attrs.value,
disabled: this.attrs.disabled
});
},
didUpdateElement() {
this.$().options({
text: this.attrs.value,
disabled: this.attrs.disabled
});
}
});
A full description of the new lifecycle hooks will be included with the documentation of Ember 1.13, but they will allow you to perform work in response to a change in attributes both before and after rendering has taken place. You will also be able to do work for initial render only, for updates only, or both.
The attrs
Property
As described above, conflating the attributes (values provided by the component's caller) with the component's own state can cause confusion, especially when combined with mutable attributes.
In particular, a seemingly innocuous change to a piece of component state can trigger a cascade of changes to child components, sibling components, and even parent components.
Beginning with angle-bracket components in Ember 1.13, a component's
attributes will be available in this.attrs
rather than on the
component itself.
So when a component is invoked this way:
<my-component title= />
The component will see this.attrs.title
as the current value of
model.name
. Whenever model.name
changes via observation, or when the
parent component is re-rendered, my-component
's lifecycle hooks will
be triggered, and it will see a new version of model.name
.
The mut
Helper
But what if you want to allow the child component to modify the property explicitly.
The mut
helper will produce an object that contains both a value
property and an update
method.
Imagine a component that will increment a count
property whenever the
component is clicked.
You might write such a component this way in Ember 1.12:
// my-counter.js
export default Component.extend({
click: function() {
this.set('count', this.get('count') + 1);
}
});
While this is fairly terse, the code in the component and its invocation
is fairly unclear. While it is modifying a property in a parent
component, nothing about the JavaScript code makes that clear. And while
the code that invokes my-counter
is allowing its activatedCount
property to be modified (which is rather unusual), nothing about the
invocation makes that clear.
Especially in larger components, and when combined with an observer on
count
, the expected behavior can become quite obtuse and it can become
hard to follow the precise, expected data flow.
In Ember 1.13, you can write the same component this way:
<my-counter count= />
// my-counter.js
export default Component.extend({
click: function() {
this.attrs.count.update(this.attrs.count.value + 1);
}
});
The call to {{mut activatedCount}}
packages up an object containing
both its current value and a callback that allows the receiving
component to modify it. There are only a handful of additional
characters, but the intent of the code is far clearer, both when the
component is invoked and when the component is updating the attribute.
In other words, {{mut}}
produces a regular JavaScript value that
contains both the current value and a way to update it. The lifecycle
hooks will fire at the same times as well.
The action
Helper
Finally, the behavior of actions in Ember 1.12 is quite magical. You
pass an action string into a component, and sendAction
triggers the
action in the environment that provided the string.
Frustratingly, it is impossible to pass a component that wants to trigger an action an alternative function, because actions are strings, not functions.
Even in Ember 1.12, you can always pass a function into a component, but
if you write something like: <my-component on-playing={{actions.playing}} />
,
the this
in the function will be wrong (you want it to be the
component). In addition, you want a way to pass arguments into the
function (i.e. "currying").
Starting in Ember 1.13, a new action
helper provides you with a way to
do both of these things:
<big-button on-active= />
<big-button on-active= />
// parent-component.js
export default Component.extend({
actions: {
selectedUser(user) {
// this is the component
// user is the user from the current iteration of the loop
}
}
});
// big-button.js
export default Component.extend({
click: function() {
this.attrs['on-active']();
}
});
Because big-button
is invoking a function, the invoking
component can provide whatever function it wants.
Another nice touch, action
works seamlessly with mut
. This means
that from the component's perspective, it's only calling a callback, but
the code that calls the component can pass in a callback that
updates one of its values.
<my-text on-enter= />
<my-text on-enter= />
// my-text.js
export default Component.extend({
keyUp(event) {
if (event.which === 13) {
this.attrs['on-enter'](this.$().val());
}
}
});
In this case, this.attrs['on-enter']
is a function. The
action
helper packages up a method in the component's actions
hash,
currying this
(and other arguments). It also converts a mut
object
into a function that can be invoked.
A component that wishes to support mutable bindings as actions need only
invoke the callback with a new value. Actions from the actions
hash,
and even regular functions passed as on-enter={{func}}
will work fine.
Routeable Components
Finally, routeable components are under active development, but there is still some uncertainty about precisely when they will land.
The complete feature has two parts:
- The ability to fully define query parameters outside the controller (in the route). Alex Matchneer and Trek Glowacki are actively working on this feature.
- The ability to have a route dispatch directly to a component, rather than to a controller and a template. Erik Bryn is actively working on this feature.
We intend to land both of these parts in the 1.x series, but there is a good chance that they will land in 1.13.1, which will be released in parallel with 2.0-beta. As I said in the beginning of this post, we will likely backport a few features to the 1.13.x series in order to ease the transition. Because of the proximity to Ember 2.0 and the impact of this change on the programming model, routable components are a good candidate for backporting.
Whenever we land these features precisely, we are committed to making sure that there is a clear way to upgrade to the last point release of 1.13, clear all deprecations, and then painlessly upgrade to 2.0.
Conclusion
Now that we have landed the Glimmer engine, we will be focusing our energy over the next few months on the release of Ember 2.0-beta on June 12 and Ember 2.0 final on July 24.
The 1.13 release is the final minor version in the 1.x series. In order to aid the transition to Ember 2.0, we plan to release several point releases of 1.13 (1.13.1, 1.13.2, etc.) that will attempt to fix regressions caused by Glimmer and do some light backporting of features that will aid in the transition.
Note that Glimmer itself implemented the full Ember 1.x test suite, so many of the regressions are easy-to-fix bugs, related to untested features, or related to private APIs. Based on experience over the past week attempting to integrate Canary into existing applications, we feel relatively optimistic about the plan of record for Ember 2.0, and where we are in the process.
Due to the recent merge of Glimmer, Ember 1.13-beta will be an unusually unstable beta release, but we are hopeful that the release of Ember 1.13.0 on June 12 will have shaken out the bulk of any problems.
If you have an app, the best thing you can do right now to help is try to upgrade it to Canary. Not all add-ons work yet, but that gap is closing every day, and the more information we can uncover through real-app testing, the faster this will go.
I would like to thank the whole Ember community for your enthusiasm and effort around the Ember 2.0 release. Maintaining a focus on compatibility while making major changes is hard, but well worth it.
Let's make this happen!