• Start Date: 2019-03-14
  • Relevant Team(s): Ember.js, Ember Data, Learning
  • RFC PR: https://github.com/emberjs/rfcs/pull/468
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/55

@classic Decorator

Summary

Add a set of warnings for users who adopt native class syntax with EmberObject base classes, and a @classic decorator which can be used to disable these warnings on a per-class basis.

Motivation

As we've added native class support to Ember and moved forward with Ember Octane, it's become increasingly clear that there are still a number of strange behaviors, edge-cases, and other issues with adopting native class syntax and extending from EmberObject. These issues include:

  • The timings of init and constructor, and how the two can interact through a class hierachy in a way that causes confusing failures.
  • The differences between class fields and properties, and how class fields can override properties even in a subclass.
  • The use of Mixins, which lack a native class alternative, and can force users to interop and have even more failure cases.

Despite these edge cases, the community has made progress using native class syntax, and it is absolutely necessary for some users (ember-cli-typescript users in particular). The ability to transition progressively to a new syntax is also very valuable, and allowing users to adopt native class syntax without completely rewriting their classes means that they can upgrade one step at a time.

However, as we begin to move into a world where more and more classes don't extend from EmberObject (starting with GlimmerComponent), these edge cases will become more common and confusing. It won't be enough to rely on simple rules of thumb like "always use init" because in some cases, there won't be an init method. It'll also become a question of "which class am I extending? GlimmerComponent? EmberComponent?

This RFC proposes adding a @classic decorator which can be used on classes that still use EmberObject APIs:

import classic from 'ember-classic-decorator';

@classic
export default class ApplicationController extends Controller {
  init() {
    super.init(...arguments);
    // setup the controller class
  }
}

This decorator will:

  1. Provide a visual hint to users that they class they are working on uses EmberObject specific functionality, and to be aware that they are not working with "just" native classes.
  2. Provide a hint to linters so that they can use a different set of rules for "classic" classes, such as always use init, etc. and native classes, such as to never use .extend() or mixins.
  3. Disable a number of warnings that will alert users when they have encountered an edge case, such as using init in a parent class and constructor in a child class.

This should allow users who wish to adopt native class syntax to do so incrementally, one change at a time, with as much safety as possible.

Detailed design

There are roughly two categories of base classes in Ember:

  1. Base classes which do not have alternatives, and use a very minimal amount of EmberObject APIs
    • Route
    • Controller
    • Service
    • Helper
  2. Base classes which have modern alternatives, and use a much larger amount of EmberObject APIs:
    • Component, which can be converted to GlimmerComponent
    • Utility classes, which can be converted plain native classes that do not extend EmberObject

@classic will work slightly differently for these two categories. We'll call them transitionable and non-transitionable respectively.

Transitionable Base Classes

Transitionable base classes can be converted to native classes safely without using any EmberObject APIs. Once converted, we can safely lint against using any EmberObject APIs, preventing any future confusion. As such, it will only be necessary to apply the @classic decorator for as long as any of the user's own classes use EmberObject APIs. These include:

  • extend
  • reopen and reopenClass
  • init
  • destroy
  • proto
  • detect
  • get/set

Along with any other extra methods or APIs that exist on CoreObject and EmberObject. Methods and APIs that exist on the class itself, such as transitionTo on Routes, or send and sendAction on Controllers, are not considered EmberObject APIs, since they could be implemented purely in native classes, and aren't tied to the way EmberObject does inheritance.

If a class, or any of its parent classes, uses one of these methods without being decorated with @classic, a lint rule will log a warning to let users know that they are using classic object APIs, and they should either refactor away from using those APIs, or use the @classic decorator to opt-out of warnings.

The @classic decorator itself will brand the class it is applied to, disabling warnings on that instance of the class.

Non-transitionable Base Classes

Non-transitionable base classes cannot be converted to native classes without relying on behaviors that are specific to EmberObject. Classic components, for instance, rely deeply on the way that EmberObject.create sets up its instance, and any utility class that extends from EmberObject must use create to create instances. While this is also true of Routes and Services, the important distinction is that users don't have to ever use create in their own code, or are not passed any arguments other than injections, so for those classes the usage of create is an implementation detail.

These classes will always warn users if they are not decorated with @classic. In order to transition away the decorator, they must be converted into newer base classes, such as GlimmerComponent, or away from base classes entirely.

Implementation

The decorator and the warnings would be added by an official Ember addon, ember-classic-decorator. This would allow us to keep the implementation details separate from the main Ember codebase, especially specifics like babel transforms for stripping out the @classic decorator in production builds.

The @classic decorator should only be applied in DEBUG mode, and should be stripped entirely from production builds. Other than that, the details of the exact implementation is left up to the champions, though it should likely use symbols to brand classes marked with the decorator.

How we teach this

In the Working with JavaScript section of the guides, when we discuss both native class syntax and classic class syntax, we should also provide a breakdown of using native classes with objects that implement or use classic class APIs. We can explain the usage of the @classic decorator here, along with clear examples for how it should be applied, and what will warnings without it.

The warnings themselve should also link to this guide page, with a clear explanation for why the warning was triggered, and a link to a section on the page that covers the specific API, and how to convert that API to use native class syntax. In the case of Glimmer components and utility classes, the sections should point to the more detailed Octane Edition guide, which covers the various differences between classic and Glimmer components and classic and native classes, and how to convert the two.

Drawbacks

Adding the @classic decorator could encourage more usage of native classes with EmberObject, which in turn could lead to even more confusion as users attempt to navigate the edge cases in differences between native and classic syntax. If the warnings provided by @classic are not strong enough, it could result in a frustrating developer experience when the differences between the two cause problems and failures.

Alternatives

This RFC is taking the stance that:

  1. Adopting native class syntax with EmberObject is valuable enough to a significant number of users that we should support it, and recommend it in some cases (such as for transitionable classes)
  2. There are enough caveats with doing this that we must warn users of them in order to support it.

We could alternatively not recommend using native class syntax with any class that extends EmberObject, even Route/Controller/Service and other transitionable classes, and wait until we have a pure native alternative to transition to.

We could also not warn, if we believe the caveats are actually not that problematic, and that users can figure out the details on their own without any hinting.

Built-in Instead of Addon

This RFC currently proposes adding @classic as a default addon, which will allow us to keep its implementation separate from the main Ember codebase. This could lead to users dropping it or not adding it to existing codebases, which would in turn lead to users encountering failures without any warnings.