If you’ve been paying attention to Ember RFC process you may have noticed that an RFC for ES Classes was accepted recently. The RFC was very minor: it didn’t propose any overhauls or breaking changes to the Ember object model as it stands. In fact, it was really just formalizing an existing oddity — ES classes work with Ember as is, right now, as far back as Ember v1.11 (and possibly even farther). You can use them today, along with class fields and decorators, and get the benefits of modern JS syntax, including:
Of course, there are some caveats with switching to classes today. Certain Ember features are currently broken and being fixed as-per the RFC. Others, like class fields, have changed dramatically, and you’ll likely need to update your mental model just a bit.
This guide will lay out all you need to know about using classes in Ember today, including some new techniques that add additional layers of safety and clarity when writing Ember code.
When you’re ready to start using classes in Ember, the first thing you’ll want to do is install ember-decorators:
$ ember install ember-decorators
This addon adds babel transforms for decorators and class fields and provides a suite of decorators for common ember functionality. These are not official Ember decorators, and technically most things can be accomplished without them, but it would be overly complicated and verbose so they are highly recommended.
Let’s start with a minimal component example:
As you can see, they’re very similar. Lifecycle hooks like didInsertElement, didReceiveAttrs, and others still work, along with event hooks like click, hover, etc. For standard classes you should still use create to make instances of the class, and other standard methods should work as expected.
The biggest difference is that we use the class constructor function instead of init. While the init hook will still work in current Ember versions, constructor should be preferred as a more semantically correct alternative. In addition, init does not work with legacy versions of Ember (more on that later).
Another major difference is that we can optionally provide a class name to the class keyword. You should always do this to ensure that instances of the class can display the name, which solves one of the oldest problems in Ember — figuring out the name of a class, especially one that isn’t instantiated via the container.
Finally, there are some major features which are currently broken and will be fixed in the future versions Ember, including:
However, nearly everything else has an equivalent alternative through class fields and decorators.
Class fields are probably the most fundamentally different part of ES classes when compared to the Ember Object model. Here’s a simple example of them that demonstrates the difference:
Under the hood, EmberObject.extend takes all the values provided to it and places them directly on the prototype of the class. This means that class fields are prototype state, not instance state, and so when we provide a value to the instance via create it gets overridden. This is also what leads to some of the common gotchas surrounding class fields, like how every instance of the class can end up sharing the same instance of an array or object on the class prototype.
By contrast, ES classes place fields directly on the instance of the class.
This means that every instance will get its own copy of the initial value. This is very useful and intuitive for the cases where we want to ensure state is not shared with other instances, but it means we have to understand the construction of our instances a bit more.
The example above translates essentially to this:
Classes have no way of modifying their superclass, and following the rules of constructors they must wait for the superclass constructor to be called before touching the instance via this, which in turn means that fields can only be instantiated after the superclass has already set all the values passed into create. It makes sense, but it’s somewhat inconvenient for Ember developers.
There are a few ways of addressing this:
The difference in placement of fields may seem small, but it has pretty large ramifications in how we write code. There are plenty of benefits to this new behavior, but it definitely takes some getting used to, especially for experienced Ember developers.
The ember-decorators addon provides decorators for:
We’ll go over each with a brief example and description of the differences and caveats. For more detailed information, check out the API docs for the library.
The @computed decorator was one of the very first demos of how decorators could be used in Ember. Early examples and the first iteration of ember-decorators, back when it was called ember-computed-decorators, used it directly in the Ember Object model. The option to use decorators on POJOs looks like it may not make it through TC39, but the decorator itself is still around and works beautifully with class syntax:
As you can see, the decorators use native ES getter/setter syntax instead of plain methods. The syntax is meant to be clearer overall and enforce method parameters, but the properties themselves must still be manipulated with get and set. For computeds which have a setter, the decorator only needs to be applied once to either the getter or setter.
The readOnly decorator can also be used to mark computeds as read only, instead of the chained method like in the original syntax. volatile computeds which normally recompute their value each time they are accessed can be replaced with a normal, native ES getter, which does this by default.
Most of the standard Ember computed macros such as alias, and, or, etc. are available in ember-decorators as well. They can be applied directly to empty class fields:
The notable exception is readOnly, which was omitted to prevent a collision with the existing @readOnly modifier. The solution here is to modify @alias or @reads with @readOnly:
Experienced Ember devs are probably familiar with the tagName, classNames, classNameBindings, and attributeBindings properties that can be used to customize a component’s element. These special properties can be a common source of confusion for new developers, and Ember is trying to move away from them in the near future with Glimmer components, but for the time being they are still necessary.
tagName is a special property that needs to be applied before the component initializes in some cases, i.e. on the component’s prototype. The other three are examples of concatenated properties — they append their values to the values of property on the superclass — which we pointed out in the beginning are currently broken in ES classes.
The solution is a combination of class and property decorators:
The class decorators allow you to specify properties of the class in advance, while the property decorators allow you to both declaratively specify the binding and the default value in a single statement.
The @service and @controller decorators exist to allow you to inject services and controllers into classes. They work very similarly to the existing syntax, the just need to be applied to an empty class field. A service name can be provided to the decorator, or it can infer the name via reflection:
The actions hash on Ember objects is the most common example of a merged property — one whose values will be merged with the actions hash of the superclass, and so on. Similar to the @attribute and @className helpers, ember-decorators provides an @action decorator which can be applied directly to class methods:
One key difference this causes is that the method exists on the class itself in the ES Class version. This means it can conflict with other lifecycle or event hooks, so be aware of name collisions.
The @on and @observes decorators will allow you to turn functions into event listeners and observers once the work has been done to fix the issues in Ember.Object. Currently, they don’t work, and one major caveat of them not working is that classes that have existing listeners or observers will not properly override those when being extended — if you have an observer or event listener named foo and you try to override it in a subclass, it will still fire.
When they are fixed, you’ll be able to use them like so:
There are decorators for @attr, @hasMany, and @belongsTo in the ember-decorators library that currently work on the standard Ember Object model using DS.Model.extend, but do not work on ES classes. For the moment, the recommendation is to continue using .extend with Ember Data models.
Integrating them into ES Classes would require a complete rework of how the the mixin system and Core Object works internally. On top of that, mixins are not an Ember-specific pattern — plenty of other frameworks have implemented them, and more systems will likely emerge as class decorators become standardized. With that in mind, the RFC’s position was that mixins should not be reworked to work with ES class syntax.
If you still really need them, however, you can continue using .extend to mix them in. When extending has been fixed for ES classes in general they will be usable anywhere:
The @ember-decorators/argument addon provides a set of decorators that accomplish two things:
The @argument decorator marks a field as an argument and sets the default if one hasn’t been set. It takes its name from Glimmer.js, which requires that users make a distinction between arguments and attributes when invoking a component. Arguments get passed into the component, while attributes get applied to the component’s element. The name also implies similarity to a function call, which is a helpful mental model for thinking about components — you are calling them from the template with some arguments, just like a function.
By default, components will throw an error when you attempt to use them with arguments that haven’t been defined. This does not apply to other types of objects, and it can be turned off via an ember-cli option:
Fields marked with the @attribute and @className decorators are also whitelisted, so they won’t throw errors.
You can use the @type, @required, and @immutable decorators to specify invariants about various fields, arguments, and attributes. The validations run once at the end of object creation, and in the case of @type and @immutable whenever you attempt to set their values.
Types can either be a string representing a primitive type, a class that the field is an instance of, or a type made using one of the type helpers: unionOf, arrayOf, or shapeOf. Some predefined types are included with the library, including:
Here’s an example showcasing the flexibility of these decorators:
All of these extra validations are stripped from production builds by default, so you won’t have to worry about them impacting the performance of your app. For more detailed usage docs, checkout the documentation.
Prior to Ember v2.13, the framework accomplished dependency injections by extending classes a second time and adding the injections. This breaks the ES class constructor function, which in turn breaks class fields.
If you’re on an older version of Ember, you can install the ember-legacy-class-shim:
$ ember install ember-legacy-class-shim
This addon reopens Ember.Object to change the behavior of the extend function when being used on native classes for injection. This does not fix extend for general usage on native classes however, as that requires changes to Ember.Object in the Ember.js core.
Now that you know how to use ES Classes in your app, you may be curious about what’s coming up next with the evolving spec!
As I noted in the beginning, upgrading to Babel 7 and the latest version of the spec should be interesting, but ember-decorators and @ember-decorators/argument should be able maintain their existing APIs. Fixes for the broken functionality like observers and events are in the works in Ember core, and fixes for the Ember Data decorators should come soon.
There are also some projects in the works to take advantage of the declarative and standardized nature of this syntax to work on better tooling for Ember users in general. The type information provided by @ember-decorators/argument can be used to automatically generate thorough component documentation, including both the arguments each component receives and the actions it sends. At some point the metadata may be usable in better static analysis tools as well (although at that point it may be better to switch to ember-cli-typescript)!
To summarize the new API, here’s an attempt at a not-totally-contrived example component that demonstrates the differences:
Overall the result is clearer and easier to read, the decorators provide context and are self-documenting, and the final component has more levels of safety than before.
Thanks for reading, and if you’re curious about the project, would like to ask questions, or wanna help out, check out the #topic-es-decorators channel on the Ember Community Slack!
If you’re looking to build ambitious apps with Ember.js and the latest in ES2017+ standards, Addepar is hiring!