ES Classes in Ember.js

Classes have been an emerging standard in Javascript for some time now. With features like class fields, private methods and fields, and decorators on the way, pretty soon Javascript will have a fully featured class syntax with capabilities that haven’t been available in the language before. While Ember.js has had its own, trusty, custom object-model in Ember.CoreObject since the very beginning, these broader advancements are going to be the future of the language and the web itself, so in time we will have to build out and replace the existing system with one based on ES2017+ classes.

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:

  • Better tooling from editors like VS Code and Webstorm
  • Better static analysis tools and documentation generators, like ES Doc
  • Shared code and solutions with a wider ecosystem, including other decorator libraries such as core-decorators
  • Shared knowledge with a wider ecosystem — no more needing to teach the Ember object model to new developers, they just need to know Javascript and they’re good to go
  • A simpler, cleaner, declarative syntax that’s easier to understand and makes your code more readable

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.

One last note before we get started: Most of this guide assumes that you’ll be using Class Fields and Decorators, which are stage 3 and 2, respectively, in the TC39 process. This means that while they definitely have a future in Javascript, they could change as time goes on. (Decorators will already be going through a major overhaul from stage 1 to 2 in Babel 7.) The ember-decorators project is dedicated to moving forward as stably as possible, but we can’t control the spec, so Your-Mileage-May-Vary.

The Basics

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:

  • Observers and event listeners
  • Merged/concatenated properties
  • Being able to use .extend to extend ES Classes

However, nearly everything else has an equivalent alternative through class fields and decorators.

Class Fields

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:

What’s Going On?

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:

  • Default values can be set in the constructor instead of as class fields.
  • Default values can be provided by an initializer. Class fields can be provided an expression which will be run for each instance of the class (like creating a new array or object, for instance).
  • Decorators like those provided in the @ember-decorators/argument addon can be used to mark the fields as arguments the object will receive, and set the default if one does not exist. We’ll touch on this approach more later.

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.

Decorators

The ember-decorators addon provides decorators for:

  • Computed Properties
  • Component Element Customization
  • Injections
  • Actions
  • Observers and Events (Currently broken with ES Classes)
  • Ember Data (Currently broken with ES Classes)

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.

Computed Properties

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.

Computed Macros

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:

Component Element Customization

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.

Injections

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:

Actions

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.

Observers and Events

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:

Ember Data

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.

Mixins

The last major Ember feature we’ll touch on are Mixins. Mixins are an integral part of Ember, they are the foundation of the Ember Object model (quite literally, they’re core to how extending works!) However, they are leftover from a time when Javascript didn’t have a class system, and each framework had to invent their own.

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:

Argument Decorators

The @ember-decorators/argument addon provides a set of decorators that accomplish two things:

  1. Provide a sane way to set defaults on components and other objects via the @argument decorator, addressing the issues brought up by the segment on class fields above.
  2. Provide runtime type and invariant validations, inspired by the excellent ember-prop-types library. These validations are completely removed from production builds by default, so they are effectively zero-cost.

Providing Defaults

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.

Specifying Validations

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:

  • Action: Union type of string and Function , and recommended for specifying actions that are passed into components
  • ClosureAction: Alias of Function and recommended if you want to enforce only closure actions
  • Element: Fastboot-safe alias for window.Element
  • Node: Fastboot-safe alias for window.Node

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.

Legacy Usage

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.

What Comes Next

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

Eventually, as decorators are finalized and accepted into Javascript, RFCs will eventually land in Ember itself for an official set of decorators. The ember-decorators project will likely continue past that, deprecating decorators that are replaced but continuing to support supplemental decorators like those in @ember-decorators/argument.

Putting It All Together

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.

In review:

  • ES Classes can be used with Ember today, as far back as Ember v1.11
  • Major differences include:
    - constructor replaces init
    - Class fields are applied to the instance, not the prototype
    - Decorators should be used for most Ember functionality, like computeds and service injection
  • Major caveats include:
    - Observers and events do not work
    - Merged and concatenated properties do not work
    .extend does not work on ES Classes themselves
    - Mixins cannot be applied to ES Classes, you must use .extend
  • If you’re using a version of Ember under 2.13, you should install ember-legacy-class-transform addon
  • Checkout @ember-decorators/argument for helpful, declarative validations and sane defaults
  • More class-y goodness is in the pipeline!

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!