Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

A Path to JavaScript Classes

Babel has recently added support for the decorator and property proposals, which makes it a good time to explore how Ember can begin to support JavaScript class syntax in the future.

Any path to JavaScript classes needs to support an incremental transition from our current class-like abstraction to class syntax, with these rough scenarios:

  • When framework classes are written as JavaScript classes, continue to support subclassing via Class.extend, including support for this._super().
  • Support subclassing Class.extend classes via JavaScript's extends clauses, including support for super().
  • Support for declarative computed properties, observers and events using the JavaScript decorator syntax.
  • A transition path for some of the more exotic features of the Ember class syntax, such as concatenated properties.

From a high-level, an Ember class that looks like this:

export default EmberComponent.extend({
  webSocket: service('web-socket'),

  init: function() {
    this._super.apply(this, arguments);
    this.get('webSocket').connect();
  },
  
  fullName: computed(function() {
    return this.get('params.first') + ' ' + this.get('params.last');
  }).property('params.first', 'params.last'),
  
  updateAge: function() {
    this.set('age', (new Date()).getFullYear() - this.birthYear);
  }.on('willRender')
});

could be rewritten as:

export default class extends EmberComponent {
  @service('web-socket') webSocket;
  
  constructor(...args) {
    super(...args);
    this.webSocket.connect();
  }
  
  @dependencies('params.first', 'params.last')
  get fullName() {
    return `${this.params.first} ${this.params.last}`
  }
  
  @on('willRender')
  updateAge() {
    this.set('age', (new Date()).getFullYear() - this.birthYear);
  }
}

This document assumes we are able to rely upon ES5 syntax, which means that the plan described relies upon support for IE9+. The community hasn't decided precisely when we will do that yet, so the exact timing of this plan is to be decided.

Mixins

In the Ember object model, a mixin is a set of properties that can be mixed into a class. Calling this._super() from the class invokes the method on the mixin. It is possible to call this._super() to delegate to another mixin.

In JavaScript, super always delegates from one link in the prototype chain to another. As a result, the most natural way to model this in JavaScript is for a class and each of its mixins to be a link in the prototype chain.

However, since mixins can be mixed into many different classes, a mixin cannot be represented as a single class. Instead, it must be represented as a function that generates a new class on demand for a given class that it is being mixed into.

So this:

// A mixin that can work with any component that supports the
// `input` event
var TextSupport = Mixin.create({
  inputChanged: function() {
    this.set('value', this.$().val());
  }.on('input'),
  
  willRender: function() {
    this._super.apply(this, arguments);
    computeDisabled(this, this.params.disabled)
  }
});

function computeDisabled(component, disabledParam) {
  // computation
}

Becomes this:

function TextSupport(parent) {
  return class extends parent {
    @on('input')
    inputChanged() {
      this.set('value', this.$().val());
    }
    
    willRender(...args) {
      super.willRender(...args);
      computeDisabled(this, this.params.disabled);
    }
  }
}

function computeDisabled(component, disabledParam) {
  // computation
}

It's a little bit noisier (because it's nested inside a function), but in order for super to work, we need each use of the mixin to be a new copy of a statically declared class.

Explicit syntax for mixins, or a revived toMethod proposal, could shrink down this syntax.

One nice thing about this mechanism is that Ember's various decorators will work just as well in mixins defined this way as they will on classes themselves.

this._super() on Methods

Ember's object model automatically creates a "super wrapper" whenever it sees a method that was also defined on the superclass.

Specifically, before applying the method, Ember does this check:

superMethod = superMethod || prototype[key];

if (superMethod !== undefined && typeof superMethod === 'function') {
  // first, some optimizations to avoid unnecessary wrapping, then...
  wrap(currentMethod, superMethod);
}

Since JavaScript classes would expose methods on the prototype in the same way, subclassing a JavaScript class using the Ember subclassing mechanism should work.

The reason this is important is that we want to move existing framework classes over to classes written using JavaScript syntax, but existing applications will still subclass them using the Ember object model, and we need this to work.

this._super on Computed Properties

Ember also supports calling this._super() on computed properties.

The way that works is that Ember checks to see whether the superclass property is a computed property. If it is a computed property, Ember applies similar wrapping logic as the previous section, but separately wraps the getter and setter.

If the superclass is a JavaScript class, Ember will have to look for JavaScript accessors, and create wrappers for the JavaScript getters and setters, rather than for Ember's computed property logic.

That should be doable.

super.foo() to Ember Classes

When subclassing an Ember class with a JavaScript class, users may want to use the JavaScript super.method() mechanism to delegate to an Ember class.

// LegacyComponent is written using the Ember class notation
class MyComponent extends LegacyComponent {
  willDestroy() {
    super.willDestroy();
  }
}

Because super.willDestroy desugars (roughly) to LegacyComponent.willDestroy.call(this), and the Ember object model produces a regular class with a regular prototype, this should work fine.

super.foo from a JavaScript Getter

Similarly, a subclass of a legacy class may want to invoke super.foo from a getter:

class MyComponent extends LegacyComponent {
  get elementId() {
    return super.elementId;
  }
}

The only good way to make this work is to define Ember computed properties written using Ember's class notation as getters. If that was the implementation, using super.elementId as in this example would work.

Reopen

The Ember object model makes it possible to "reopen" an existing class or mixin, and add new properties.

Importantly, if a newly provided property clobbers an existing property, it is possible for the newly provided property to call this._super() to the original definition.

It is possible to model this behavior using JavaScript classes:

// The framework's definition of Component
class Component {

}

export default class extends Component {
  static reopen(newClass) {
    let original = this.prototype;
    newClass.prototype = original;
    this.prototype = newClass;
  }
}

This would mean that existing Ember classes would need to place a facade in front of the actual definition of the class in order to support reopening, and simple definitions of JavaScript classes could not support reopening with a stable identity.

In practice, this means that while we can continue to support the existing semantics for framework classes, we will not be able to support reopen for new user classes written using JavaScript syntax. This likely means that we would deprecate reopen, maintaining medium-term compatibility using the facade strategy.

Timing

In the Ember object model, decorators (like property) modify the function they are applied to, and the extend method loops over all methods, extracts the metadata, and sets up per-class and per-instance state at extension time.

With JavaScript classes, we are not notified when a class is extended, but we can make use of the way decorators work to achieve similar goals.

Concatenated Properties

In JavaScript classes, class decorators are a better way to describe concatenated properties:

class MyElement extends Component {
  @concat classNames = ['my-element', 'my-button'];
}

That would effectively expand to:

class MyElement extends Component {
  get classNames() {
    return super.classNames.concat(['my-element', 'my-button']);
  }
}

When subclassing an Ember class that used concatenated properties, this mechanism would work, because the closest Ember superclass will have already resolved the concatenated property.

A class written using JavaScript syntax that wants to support Ember subclasses that expect concatenated properties can support such subclasses:

class Component {
  concatenatedProperties = ['classNames'];
  classNames = ['ember-view'];
}

Because Ember's extend mechanism simply looks for a property named concatenatedProperties on the prototype it's extending, specifying such a property will have the desired effect.

ClassMixin and PrototypeMixin

One detail elided in the above descriptions is the fact that Ember classes support class-side and instance-side inheritance by expecting to work with an object that has a ClassMixin and PrototypeMixins.

The Ember object system will have to be extended to be able to support regular JavaScript classes as superclasses, in the ways described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.