Deprecations Added in Ember 3.x

What follows is a list of deprecations introduced to Ember during the 3.x cycle.

For more information on deprecations in Ember, see the main deprecations page.

Deprecations Added in 3.0

§ Old deprecate method imports

until: 4.0.0
id: old-deprecate-method-paths

Importing deprecate from @ember/application/deprecations has been deprecated. Please update to import { deprecate } from '@ember/debug'.

Deprecations Added in 3.1

§ Getting the @each property

until: 3.5.0
id: getting-the-each-property

Calling array.get('@each') is deprecated. @each may only be used as dependency key.

§ Use notifyPropertyChange instead of propertyWillChange and propertyDidChange

until: 3.5.0
id: use-notifypropertychange-instead-of-propertywillchange-and-propertydidchange

Ember.Application#registry / Ember.ApplicationInstance#registry

The private APIs propertyWillChange and propertyDidChange will be removed after the first LTS of the 3.x cycle. You should remove any calls to propertyWillChange and replace any calls to propertyDidChange with notifyPropertyChange. This applies to both the Ember global version and the EmberObject method version.

For example, the following:

Ember.propertyWillChange(object, 'someProperty');
doStuff(object);
Ember.propertyDidChange(object, 'someProperty');

object.propertyWillChange('someProperty');
doStuff(object);
object.propertyDidChange('someProperty');

Should be changed to:

doStuff(object);
Ember.notifyPropertyChange(object, 'someProperty');

doStuff(object);
object.notifyPropertyChange('someProperty');

If you are an addon author and need to support both Ember applications greater than 3.1 and less than 3.1 you can use the polyfill ember-notify-property-change-polyfill

Deprecations Added in 3.2

§ Use console rather than Ember.Logger

until: 4.0.0
id: ember-console.deprecate-logger

Use of Ember.Logger is deprecated. You should replace any calls to Ember.Logger with calls to console.

In Edge and IE11, uses of console beyond calling its methods may require more subtle changes than simply substituting console wherever Logger appears. In these browsers, they will behave just as they do in other browsers when your development tools window is open. However, when run normally, calls to its methods must not be bound to anything other than the console object or you will receive an Invalid calling object exception. This is a known inconsistency with these browsers. (Edge issue #14495220.)

To avoid this, transform the following:

var print = Logger.log; // assigning method to variable

to:

// assigning method bound to console to variable
var print = console.log.bind(console);

Also, transform any of the following:

Logger.info.apply(undefined, arguments); // or
Logger.info.apply(null, arguments); // or
Logger.info.apply(this, arguments); // or

to:

console.info.apply(console, arguments);

Finally, because node versions before version 9 don't support console.debug, you may want to transform the following:

Logger.debug(message);

to:

if (console.debug) {
  console.debug(message);
} else {
  console.log(message);
}

Add-on Authors

If your add-on needs to support both Ember 2.x and Ember 3.x clients, you will need to test for the existence of console before calling its methods. If you do much logging, you may find it convenient to define your own wrapper. Writing the wrapper as a service will provide for dependency injection by tests and perhaps even clients.

§ Use defineProperty to define computed properties

until: 3.5.0
id: ember-meta.descriptor-on-object

Although uncommon, it is possible to assign computed properties directly to objects and have them be implicitly computed from eg Ember.get. As part of supporting ES5 getter computed properties, assigning computed properties directly is deprecated. You should replace these assignments with calls to defineProperty.

For example, the following:

let object = {};
object.key = Ember.computed(() => 'value');
Ember.get(object, 'key') === 'value';

Should be changed to:

let object = {};
Ember.defineProperty(object, 'key', Ember.computed(() => 'value'));
Ember.get(object, 'key') === 'value';

§ Private property Route.router has been renamed to Route._router

until: 3.5.0
id: ember-routing.route-router

The Route#router private API has been renamed to Route#_router to avoid collisions with user-defined properties or methods. If you want access to the router, you are probably better served injecting the router service into the route like this:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  router: service()
});

Deprecations Added in 3.3

§ Old extend prototypes

until: 4.0.0
id: ember-env.old-extend-prototypes

Accessing Ember.EXTEND_PROTOTYPES is deprecated.

If you need to access the consuming application's EXTEND_PROTOTYPES configuration in your addon, you can do the following:

my-addon/addon/services/my-awesome-service.js
import { getOwner } from "@ember/application";
import Service from "@ember/service";

export default class MyAwesomeService extends Service {
  myMethod() {
    const ENV = getOwner(this).resolveRegistration("config:environment");
    if (ENV.EmberENV.EXTEND_PROTOTYPES) {
      // ... do something
    }
  }
}

As a reminder, disabling prototype extensions in an Ember.js application is done by setting EmberENV.EXTEND_PROTOTYPES in config/environment.js.

config/environment.js
ENV = {
  EmberENV: {
    EXTEND_PROTOTYPES: false
  }
}

§ Use ember-copy addon instead of copy method and Copyable mixin.

until: 4.0.0
id: ember-runtime.deprecate-copy-copyable

Since Ember's earliest days, the copy function and Copyable mixin from @ember/object/internals were intended to be treated as an Ember internal mechanism. The Copyable mixin, in particular, has always been marked private, and it is required in order to use copy with any Ember Object-derived class without receiving an assertion.

Copyable hasn't been used by any code inside of Ember for a very long time, except for the NativeArray mixin, inherited by Ember arrays. The deprecated copy function now handles array copies directly, no longer delegating to NativeArray.copy. With this deprecation, NativeArray no longer inherits from Copyable and its implementation of copy is also deprecated.

For shallow copies of data where you use copy(x) or copy(x, false), the ES6 Object.assign({}, x) will provide the desired effect. For deep copies, copy(x, true), the most efficient and concise approach varies with the situation, but several options are available in open source.

For those whose code is deeply dependent upon the existing implementation, copy and Copyable have been extracted to the ember-copy addon . If you are only using documented methods, this will only require adjusting your import statements to use the methods from ember-copy instead of @ember/object/internals. The code in the addon should work identically to what you were using before.

§ Use native events instead of jQuery.Event

until: 4.0.0
id: jquery-event

As part of the effort to decouple Ember from jQuery, using event object APIs that are specific to jQuery.Event such as originalEvent are deprecated. Especially addons are urged to not use any jQuery specific APIs, so they are able to work in a world without jQuery.

Using native events

jQuery events copy most of the properties of their native event counterpart, but not all of them. See the jQuery.Event API for further details. These properties will work with jQuery events as well as native events, so just use them without originalEvent.

Before:

// your event handler:
click(event) {
  let x = event.originalEvent.clientX;
  ...
}

After:

// your event handler:
click(event) {
  let x = event.clientX;
  ...
}

For those other properties it was necessary to get access to the native event object through originalEvent though. To prevent your code from being coupled to jQuery, use the normalizeEvent function provided by ember-jquery-legacy, which will work with or without jQuery to provide the native event without triggering any deprecations.

ember install ember-jquery-legacy

Before:

// your event handler:
click(event) {
  let nativeEvent = event.originalEvent;
  ...
}

After:

import { normalizeEvent } from 'ember-jquery-legacy';

// your event handler:
click(event) {
  let nativeEvent = normalizeEvent(event);
  ...
}

Opting into jQuery

For apps which are ok to work only with jQuery, you can explicitly opt into the jQuery integration and thus quash the deprecations:

ember install @ember/jquery
ember install @ember/optional-features
ember feature:enable jquery-integration

Deprecations Added in 3.4

§ Use closure actions instead of sendAction

until: 4.0.0
id: ember-component.send-action

In Ember 1.13 closure actions were introduced as a recommended replacement for sendAction. With sendAction the developer passes the name of an action, and when sendAction is called Ember.js would look up that action in the parent context and invoke it if present. This had a handful of caveats:

  • Since the action is not looked up until it's about to be invoked, it's easier for a typo in the action's name to go undetected.

  • Using sendAction you cannot receive the return value of the invoked action.

Closure actions solve those problems and on top are also more intuitive to use.

app/controllers/index.js
export default Controller.extend({
  actions: {
    sendData(data) {
      fetch('/endpoint', { body: JSON.stringify(data) });
    }
  }
})
app/templates/index.hbs
{{my-component submit="sendData"}}
app/components/my-component.js
this.sendAction('submit');

Should be changed to:

app/controllers/index.js
export default Controller.extend({
  actions: {
    sendData(data) {
      fetch('/endpoint', { body: JSON.stringify(data) });
    }
  }
})
app/templates/index.hbs
{{my-component submit=(action "sendData")}}
app/components/my-component.js
export default Component.extend({
  click() {
    this.submit();
  }
});

Note that with this approach the component MUST receive that submit property, while with sendAction if it didn't it would silently do nothing.

If you don't want submit to be mandatory, you have to check for the presence of the action before calling it:

export default Component.extend({
  click() {
    if (this.submit) {
      this.submit();
    }
  }
});

Another alternative is to define an empty action on the component, which helps clarify that the function is not mandatory:

app/components/my-component.js
export default Component.extend({
  submit: () => {},
  //...
  click() {
    this.submit();
  }
});

This deprecation also affects the built-in {{input}} helper that used to allow passing actions as strings:

{{input enter="handleEnter"}}

Since this uses sendAction underneath it is also deprecated and must also be replaced by closure actions:

{{input enter=(action "handleEnter")}}

Deprecations Added in 3.6

§ Calling A as a constructor

until: 3.9.0
id: array.new-array-wrapper

The A function imported from @ember/array is a function that can be used to apply array mixins to an existing object (generally a native array):

import { A } from '@ember/array';

let arr = [];

A(arr);

arr.pushObject(1);

A will also return the "wrapped" array for convenience, and if no array is passed will create the array instead:

let arr1 = A([]);
let arr2 = A();

Because A is a standard function, it can also be used as a constructor. The constructor does not actually do anything different (because Javascript constructors can return something other than an instance). This was not intended behavior - A was originally implemented as an arrow function which cannot be used as a constructor, but as a side effect of transpilation it was turned into a normal function which could.

To update, remove any usage of new with A, and call A as a standard function. Before:

let arr = new A();

After:

let arr = A();

If linting rules prevent you from doing this, rename A to indicate that it is a function and not a constructor:

import { A as emberA } from '@ember/array';

let arr = emberA();

§ Router Events

until: 4.0.0
id: deprecate-router-events

Application-wide transition monitoring events belong on the Router service, not spread throughout the Route classes. That is the reason for the existing willTransition and didTransition hooks/events on the Router. But they are not sufficient to capture all the detail people need.

In addition, they receive handlerInfos in their arguments, which are an undocumented internal implementation detail of router.js that doesn't belong in Ember's public API. Everything you can do with handlerInfos can be done with the RouteInfo.

Below is how you would transition Router usages of willTransition and didTransition.

From:

import Router from '@ember/routing/router';
import { inject as service } from '@ember/service';

export default Router.extend({
  currentUser: service('current-user'),

  willTransition(transition) {
    this._super(...arguments);
    if (!this.currentUser.isLoggedIn) {
      transition.abort();
      this.transitionTo('login');
    }
  },

  didTransition(privateInfos) {
    this._super(...arguments);
    ga.send('pageView', {
      pageName: privateInfos.name
    });
  }
});

To:

import Router from '@ember/routing/router';
import { inject as service } from '@ember/service';

export default Router.extend({
  currentUser: service('current-user'),

  init() {
    this._super(...arguments);
    this.on('routeWillChange', transition => {
      if (!this.currentUser.isLoggedIn) {
        transition.abort();
        this.transitionTo('login');
      }
    });

    this.on('routeDidChange', transition => {
      ga.send('pageView', {
        pageName: transition.to.name
      });
    });
  }
});

§ Ember.merge

until: 4.0.0
id: ember-polyfills.deprecate-merge

Ember.merge predates Ember.assign, but since Ember.assign has been released, Ember.merge has been mostly unnecessary. To cut down on duplication, we are now recommending using Ember.assign instead of Ember.merge. If you need to support Ember <= 2.4 you can use ember-assign-polyfill to make Ember.assign available to you.

Before:

import { merge } from '@ember/polyfills';

var a = { first: 'Yehuda' };
var b = { last: 'Katz' };
merge(a, b); // a == { first: 'Yehuda', last: 'Katz' }, b == { last: 'Katz' }

After:

import { assign } from '@ember/polyfills';

var a = { first: 'Yehuda' };
var b = { last: 'Katz' };
assign(a, b); // a == { first: 'Yehuda', last: 'Katz' }, b == { last: 'Katz' }

§ Remove All Listeners/Observers

until: 3.9.0
id: events.remove-all-listeners

When using both the removeListener and removeObserver methods, users can omit the final string or method argument to trigger an undocumented codepath that will remove all event listeners/observers for the given key:

let foo = {
  method1() {}
  method2() {}
};

addListener(foo, 'init', 'method1');
addListener(foo, 'init', 'method2');

removeListener(foo, 'init');

This functionality will be removed since it is uncommonly used, undocumented, and adds a fair amount of complexity to a critical path. To update, users should remove each listener individually:

let foo = {
  method1() {}
  method2() {}
};

addListener(foo, 'init', 'method1');
addListener(foo, 'init', 'method2');

removeListener(foo, 'init', 'method1');
removeListener(foo, 'init', 'method2');

§ new EmberObject

until: 3.9.0
id: object.new-constructor

We are deprecating usage of new EmberObject() to construct instances of EmberObject and it's subclasses. This affects all classes that extend from EmberObject as well, including user defined classes and Ember classes such as:

  • Component
  • Controller
  • Service
  • Route
  • Model

Instead, you should use EmberObject.create() to create new instances of classes that extend from EmberObject. If you are using native class syntax instead of EmberObject.extend() to define your classes, you can also refactor to not extend from EmberObject, and continue to use new syntax.

Refactoring to use create() instead of new

Before this deprecation, new EmberObject() and EmberObject.create() were functionally the same, with one difference - new EmberObject() could only receive 1 argument, whereas EmberObject.create() could receive several. Because new was strictly less powerful, you can safely refactor existing code to call create with the same arguments as before:

Before:

let obj1 = new EmberObject();
let obj2 = new EmberObject({ prop: 'value' });

const Foo = EmberObject.extend();
let foo = new Foo({ bar: 123 });

After:

let obj1 = EmberObject.create();
let obj2 = EmberObject.create({ prop: 'value' });

const Foo = EmberObject.extend();
let foo = Foo.create({ bar: 123 })

Refactoring native classes to not extend from EmberObject

If you are using native class syntax to extend from EmberObject, you can instead define your classes without a base class. This means that you will have to write your own constructor function:

Before:

class Person extends EmberObject {}

let rwjblue = new Person({ firstName: 'Rob', lastName: 'Jackson' });

After:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

let rwjblue = new Person('Rob', 'Jackson');

This is closer to the way native classes are meant to work, and can help with low level performance concerns such as shaping. It also enforces clear interfaces which can help define the purpose of a class more transparently.

§ HandlerInfos Removal

until: 3.9.0
id: remove-handler-infos

HandlerInfo was a private API that has been renamed to RouteInfo to align with the router service RFC. If you need access to information about the routes, you are probably better served injecting the router service as it exposes a publically supported version of the RouteInfos. You can access them in the following ways:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  router: service(),
  init() {
    this._super(...arguments);
    this.router.on('routeWillChange', transition => {
      let { to: toRouteInfo, from: fromRouteInfo } = transition;
      console.log(`Transitioning from -> ${fromRouteInfo.name}`);
      console.log(`to -> ${toRouteInfo.name}`);
    });

    this.router.on('routeDidChange', transition => {
      let { to: toRouteInfo, from: fromRouteInfo } = transition;
      console.log(`Transitioned from -> ${fromRouteInfo.name}`);
      console.log(`to -> ${toRouteInfo.name}`);
    });
  }

  actions: {
    sendAnalytics() {
      let routeInfo = this.router.currentRoute;
      ga.send('pageView', {
        pageName: routeInfo.name,
        metaData: {
          queryParams: routeInfo.queryParams,
          params: routeInfo.params,
        }
      });
    }
  }
});

§ Transition State Removal

until: 3.9.0
id: transition-state

The Transition object is a public interface that actually exposed internal state used by router.js to perform routing. Accessing state, queryParams or params on the Transition has been removed. If you need access to information about the routes, you are probably better served injecting the router service as it exposes a publically supported version of the RouteInfos. You can access them in the following ways:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  router: service(),
  init() {
    this._super(...arguments);
    this.router.on('routeWillChange', transition => {
      let { to: toRouteInfo, from: fromRouteInfo } = transition;
      if (fromRouteInfo) {
        console.log(`Transitioning from -> ${fromRouteInfo.name}`);
        console.log(`From QPs: ${JSON.stringify(fromRouteInfo.queryParams)}`);
        console.log(`From Params: ${JSON.stringify(fromRouteInfo.params)}`);
        console.log(`From ParamNames: ${fromRouteInfo.paramNames.join(', ')}`);
      }

      if (toRouteInfo) {
        console.log(`to -> ${toRouteInfo.name}`);
        console.log(`To QPs: ${JSON.stringify(toRouteInfo.queryParams)}`);
        console.log(`To Params: ${JSON.stringify(toRouteInfo.params)}`);
        console.log(`To ParamNames: ${toRouteInfo.paramNames.join(', ')}`);
      }
    });

    this.router.on('routeDidChange', transition => {
      let { to: toRouteInfo, from: fromRouteInfo } = transition;
      if (fromRouteInfo) {
        console.log(`Transitioned from -> ${fromRouteInfo.name}`);
        console.log(`From QPs: ${JSON.stringify(fromRouteInfo.queryParams)}`);
        console.log(`From Params: ${JSON.stringify(fromRouteInfo.params)}`);
        console.log(`From ParamNames: ${fromRouteInfo.paramNames.join(', ')}`);
      }

      if (toRouteInfo) {
        console.log(`to -> ${toRouteInfo.name}`);
        console.log(`To QPs: ${JSON.stringify(toRouteInfo.queryParams)}`);
        console.log(`To Params: ${JSON.stringify(toRouteInfo.params)}`);
        console.log(`To ParamNames: ${toRouteInfo.paramNames.join(', ')}`);
      }
    });
  }

  actions: {
    sendAnalytics() {
      let routeInfo = this.router.currentRoute;
      ga.send('pageView', {
        pageName: routeInfo.name,
        metaData: {
          queryParams: routeInfo.queryParams,
          params: routeInfo.params,
        }
      });
    }
  }
});

Deprecations Added in 3.8

§ Component Manager Factory Function

until: 4.0.0
id: component-manager-string-lookup

setComponentManager no longer takes a string to associate the custom component class and the component manager. Instead you must pass a factory function that produces an instance of the component manager.

Before:

import { setComponentManager } from '@ember/component';
import BasicComponent from './component-class';

setComponentManager('basic', BasicComponent);

After:

import { setComponentManager } from '@ember/component';
import BasicComponent from './component-class';
import BasicManager from './component-manager';

setComponentManager(owner => {
  return new BasicManager(owner)
}, BasicComponent);

Deprecations Added in 3.9

§ Application controller router properties

until: 4.0.0
id: application-controller.router-properties

If you are reliant on the currentPath and currentRouteName properties of the ApplicationController, you can get the same functionality from the Router service.

To migrate, inject the Router service and read the currentRouteName off of it.

Before:

app/controllers/application.js
import Controller from '@ember/controller';
import fetch from 'fetch';

export default Controller.extend({
  store: service('store'),

  actions: {
    sendPayload() {
      fetch('/endpoint', {
        method: 'POST',
        body: JSON.stringify({
          route: this.currentRouteName
        })
      });
    }
  }
})

After:

app/controllers/application.js
import Controller from '@ember/controller';
import fetch from 'fetch';

export default Controller.extend({
  store: service('store'),
  router: service('router'),

  actions: {
    sendPayload() {
      fetch('/endpoint', {
        method: 'POST',
        body: JSON.stringify({
          route: this.router.currentRouteName
        })
      });
    }
  }
})

§ Computed Property Overridability

until: 4.0.0
id: computed-property.override

Ember's computed properties are overridable by default if no setter is defined:

const Person = EmberObject.extend({
  firstName: 'Diana',
  lastName: 'Prince',

  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

let person = Person.create();
person.fullName; // Diana Prince

person.set('fullName', 'Carol Danvers');

person.set('firstName', 'Bruce');
person.set('lastName', 'Wayne');


person.fullName; // Carol Danvers

This behavior is bug prone and has been deprecated. readOnly(), the modifier that prevents this behavior, will be deprecated once overridability has been removed.

If you still need this behavior, you can create a setter which accomplishes this manually:

const Person = EmberObject.extend({
  firstName: 'Diana',
  lastName: 'Prince',

  fullName: computed('firstName', 'lastName', {
    get() {
      if (this._fullName) {
        return this._fullName;
      }

      return `${this.firstName} ${this.lastName}`;
    },

    set(key, value) {
      return this._fullName = value;
    }
  })
});

§ Computed Property .property() Modifier

until: 4.0.0
id: computed-property.property

.property() is a modifier that adds additional property dependencies to an existing computed property:

const Person = EmberObject.extend({
  fullName: computed(function() {
    return `${this.firstName} ${this.lastName}`;
  }).property('firstName', 'lastName')
});

To update, move the dependencies to the main computed property definition:

const Person = EmberObject.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

In the case of the filter, map, and sort computed property macros, it was previously possible to need to add dependencies because they weren't available in the public APIs of those macros. An optional second parameter has now been added to these macros which is an array of additional dependent keys, allowing you to pass these dependencies to them.

Before:

const Person = EmberObject.extend({
  friendNames: map('friends', function(friend) {
    return friend[this.get('nameKey')];
  }).property('nameKey')
});

After:

const Person = EmberObject.extend({
  friendNames: map('friends', ['nameKey'], function(friend) {
    return friend[this.get('nameKey')];
  })
});

Custom computed property macros that encounter this issue should also be refactored to be able to receive the additional keys as parameters.

§ Computed Property Volatility

until: 4.0.0
id: computed-property.volatile

NOTE: There is a bug in Native Getters in 3.9 that was fixed in 3.10. To upgrade to 3.9 directly, just add this to your deprecation workflow, and make the recommended fixes when you move to 3.10 or beyond.

.volatile() is a computed property modifier which makes a computed property recalculate every time it is accessed, instead of caching. It also prevents property notifications from ever occuring on the property, which is generally not the behavior that developers are after. Volatile properties are usually used to simulate the behavior of native getters, which means that they would otherwise behave like normal properties.

To update, use native getters directly instead:

Before:

const Person = EmberObject.extend({
  fullName: computed(function() {
    return `${this.firstName} ${this.lastName}`;
  }).volatile()
});

After:

const Person = EmberObject.extend({
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
});

§ Replace jQuery APIs

until: 4.0.0
id: jquery-apis

As of Ember 3.4.0, Ember no longer requires that all applications include jQuery, therefore APIs that are coupled to jQuery have been deprecated.

Since jQuery is not needed by Ember itself anymore, and many apps (e.g. mobile apps) are sensitive about performance, it is often beneficial for those to avoid shipping jQuery. If this is not a major concern for your app, and you see value in using jQuery, it is absolutely fine to continue doing so. It is just not included by default anymore, so you have to opt in to using it with the @ember/jquery package as described below.

For addons it is a bit different as they are not aware of the context in which they are used. Any addon that still relies on jQuery will either force their consuming apps to continue bundling jQuery, or will not be usable for apps that decide not to do so. Therefore it is highly recommended to avoid relying on jQuery in general, unless there is a good reason (e.g. an addon wrapping a jQuery plugin).

Added deprecations

The main jQuery integration API that has been deprecated is this.$() inside of an Ember.Component, which would give you a jQuery object of the component's element. Instead, you can use the this.element property, which provides a reference to a native DOM element:

import Component from '@ember/component';

export default Component.extend({
  waitForAnimation() {
    this.$().on('transitionend', () => this.doSomething());
  }
});

should be changed to:

import Component from '@ember/component';

export default Component.extend({
  waitForAnimation() {
    this.element.addEventListener('transitionend', () => this.doSomething());
  }
});

If you used this.$() to query for child elements, you can do so as well with native DOM APIs:

import Component from '@ember/component';

export default Component.extend({
  waitForAnimation() {
    this.$('.animated').on('transitionend', () => this.doSomething());
  }
});

should be changed to:

import Component from '@ember/component';

export default Component.extend({
  waitForAnimation() {
    this.element.querySelectorAll('.animated')
      .forEach((el) => el.addEventListener('transitionend', () => this.doSomething()));
  }
});

This applies in a similar fashion to component tests using the setupRenderingTest() helper. Instead of using this.$() in a test, you should use this.element (or alternatively the find()/findAll() helpers from @ember/test-helpers):

test('it disables the button', async function(assert) {
  // ...

  assert.ok(this.$('button').prop('disabled'), 'Button is disabled');
});

should be changed to:

test('it disables the button', async function(assert) {
  // ...

  assert.ok(this.element.querySelector('button').disabled, 'Button is disabled');
});

If you do continue to use jQuery, please make sure to always import it like this:

import jQuery from 'jquery';

Accessing it from the Ember namespace as Ember.$ is and will remain deprecated.

Opting into jQuery

Apps and addons which require jQuery, can opt into the jQuery integration now provided by the @ember/jquery package. This will provide the this.$() API to Ember.Components, and will make sure that the EventDispatcher will provide jQuery events to a component's event handler methods to maintain compatibility. this.$() deprecation warnings will still be displayed.

ember install @ember/jquery
ember install @ember/optional-features
ember feature:enable jquery-integration

For addons make sure that @ember/jquery is added as a dependency in its package.json!

§ @ember/object#aliasMethod

until: 4.0.0
id: object.alias-method

@ember/object#aliasMethod is a little known and rarely used method that allows user's to add aliases to objects defined with EmberObject:

import EmberObject, { aliasMethod } from '@ember/object';

export default EmberObject.extend({
  foo: 123,
  bar() {
    console.log(this.foo);
  },
  baz: aliasMethod('bar'),
});

This can be refactored into having one function call the other directly:

import EmberObject from '@ember/object';

export default EmberObject.extend({
  foo: 123,
  bar() {
    console.log(this.foo);
  },
  baz() {
    this.bar(...arguments);
  },
});

Avoid defining methods directly on the class definition, since this will not translate well into native class syntax in the future:

// Do not use this, this is an antipattern! 🛑
import EmberObject from '@ember/object';

function logFoo() {
  console.log(this.foo);
}

export default EmberObject.extend({
  foo: 123,
  bar: logFoo,
  baz: logFoo,
});

Deprecations Added in 3.11

§ Function.prototype.observes

until: 4.0.0
id: function-prototype-extensions.observes

Historically, Ember has extended the Function.prototype with a few functions (on, observes, property), over time we have moved away from using these prototype extended functions in favor of using the official ES modules based API.

In order to migrate away from Function.prototype.observes you would update to using observer from @ember/object (see documentation) directly.

For example, you would migrate from:

import EmberObject from '@ember/object';

export default EmberObject.extend({
  valueObserver: function() {
    // Executes whenever the "value" property changes
  }.observes('value')
});

Into:

import EmberObject, { observer } from '@ember/object';

export default EmberObject.extend({
  valueObserver: observer('value', function() {
    // Executes whenever the "value" property changes
  })
});

Please review the deprecation RFC over at emberjs/rfcs for more details.

§ Function.prototype.on

until: 4.0.0
id: function-prototype-extensions.on

Historically, Ember has extended the Function.prototype with a few functions (on, observes, property), over time we have moved away from using these prototype extended functions in favor of using the official ES modules based API.

In order to migrate away from Function.prototype.on you would update to using @ember/object/evented (see documentation) directly.

For example, you would migrate from:

import EmberObject from '@ember/object';
import { sendEvent } from '@ember/object/events';

let Job = EmberObject.extend({
  logCompleted: function() {
    console.log('Job completed!');
  }.on('completed')
});

let job = Job.create();

sendEvent(job, 'completed'); // Logs 'Job completed!'

Into:

import EmberObject from '@ember/object';
import { on } from '@ember/object/evented';
import { sendEvent } from '@ember/object/events';

let Job = EmberObject.extend({
  logCompleted: on('completed', function() {
    console.log('Job completed!');
  })
});

let job = Job.create();

sendEvent(job, 'completed'); // Logs 'Job completed!'

Please review the deprecation RFC over at emberjs/rfcs for more details.

§ Function.prototype.property

until: 4.0.0
id: function-prototype-extensions.property

Historically, Ember has extended the Function.prototype with a few functions (on, observes, property), over time we have moved away from using these prototype extended functions in favor of using the official ES modules based API.

In order to migrate away from Function.prototype.property you would update to using computed from @ember/object (see documentation) directly.

For example, you would migrate from:

import EmberObject from '@ember/object';

let Person = EmberObject.extend({
  init() {
    this._super(...arguments);

    this.firstName = 'Betty';
    this.lastName = 'Jones';
  },

  fullName: function() {
    return `${this.firstName} ${this.lastName}`;
  }.property('firstName', 'lastName')
});

let client = Person.create();

client.get('fullName'); // 'Betty Jones'

client.set('lastName', 'Fuller');
client.get('fullName'); // 'Betty Fuller'

Into:

import EmberObject, { computed } from '@ember/object';

let Person = EmberObject.extend({
  init() {
    this._super(...arguments);

    this.firstName = 'Betty';
    this.lastName = 'Jones';
  },

  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

let client = Person.create();

client.get('fullName'); // 'Betty Jones'

client.set('lastName', 'Fuller');
client.get('fullName'); // 'Betty Fuller'

Please review the deprecation RFC over at emberjs/rfcs for more details.

Deprecations Added in 3.13

§ mouseEnter/Leave/Move events in {{action}} modifier

until: 4.0.0
id: action.mouseenter-leave-move

As mouseenter, mouseleave and mousemove events fire very frequently, are rarely used and have a higher implementation cost, support for them in Ember's EventDispatcher has been deprecated. As such these events should not be used with the {{action}} modifier anymore.

Before:

<button {{action "handleMouseEnter" on="mouseEnter"}}>Hover</button>

After:

<button {{on "mouseenter" this.handleMouseEnter}}>Hover</button>

§ mouseEnter/Leave/Move component methods

until: 4.0.0
id: component.mouseenter-leave-move

As mouseenter, mouseleave and mousemove events fire very frequently, are rarely used and have a higher implementation cost, support for them in Ember's EventDispatcher has been deprecated. As such the corresponding event handler methods in Ember.Component should not be used anymore.

Before:

import Component from '@ember/component';

export default class MyComponent extends Component {
  mouseEnter(e) {
    // do something
  }
}

After:

import Component from '@ember/component';
import { action } from '@ember/object';

export default class MyComponent extends Component {
  @action
  handleMouseEnter(e) {
    // do something
  }

  didInsertElement() {
    super.didInsertElement(...arguments);
    this.element.addEventListener('mouseenter', this.handleMouseEnter);
  }

  willDestroyElement() {
    super.willDestroyElement(...arguments);
    this.element.removeEventListener('mouseenter', this.handleMouseEnter);
  }
}

An alternative to attaching the event listener in the component class is to opt into outer HTML semantics by making the component tag-less and using the {{on}} modifier in the template:

import Component from '@ember/component';
import { action } from '@ember/object';

export default class MyComponent extends Component {
  tagName = '';

  @action
  handleMouseEnter(e) {
    // do something
  }
}
<div {{on "mouseenter" this.handleMouseEnter}}>
  ...
</div>

Deprecations Added in 3.15

§ Component#isVisible

until: 4.0.0
id: ember-component.is-visible

Classic components have a number of APIs to handle the wrapper div that they create by default. One of them is isVisible, which controls if the component is hidden to the end user or not. isVisible is now deprecated in accordance with RFC #324.

You can update your component in one of two ways, you can wrap your component's template in an {{if}}, or you can use the hidden HTML attribute.

It's worth noting that not all visibility approaches are equal- we recommend reviewing use of aria-hidden as well as accessible ways to visibly hide content while still making it available to assistive technology.

Because classic components have a wrapper div element by default, it might be necessary that you do additional changes to your component so that no content is accidentally shown.

Let's say you have a flash message component that hides itself when you dismiss it:

app/components/flash-message.js
import Component from '@ember/component';

export default Component.extend({
  isVisible: true,

  dismissMessage() {
    this.set('isVisible', false);
  }
});
app/components/flash-message.hbs
<p>You received a message: "{{@message}}"</p>

<button type="button" {{action 'dismissMessage'}}>Dismiss</button>

Wrapping template in an {{if}}

Fist, let's use a different property to keep track of visibility. I decided to call it shouldShow, as that name has no meaning to the classic component class:

app/components/flash-message.js
import Component from '@ember/component';

export default Component.extend({
  shouldShow: true,

  dismissMessage() {
    this.set('shouldShow', false);
  }
});

Now we wrap the template in a conditional:

app/components/flash-message.hbs
{{#if this.shouldShow}}
  <p>You received a message: "{{@message}}"</p>

  <button type="button" {{action 'dismissMessage'}}>Dismiss</button>
{{{/if}}}

As mentioned this has the drawback of still rendering the wrapping div, so next we'll see how we can use that to our advantage!

Using the hidden HTML attribute

There is an HTML attribute that you can use whenever you want an element to now show to the end user, the hidden attribute. To update our FlashMessage component to use it, we need to use the attributeBindings API.

To avoid confusion about the state of the component, we will use the shouldHide name for the property that holds the state, and flip the values:

app/components/flash-message.js
import Component from '@ember/component';

export default Component.extend({
  attributeBindings: ['shouldHide:hidden'],
  shouldHide: false,

  dismissMessage() {
    this.set('shouldHide', true);
  }
});

The template remains the same in this case:

app/components/flash-message.hbs
<p>You received a message: "{{@message}}"</p>

<button type="button" {{action 'dismissMessage'}}>Dismiss</button>

Using a Glimmer component

If you are looking to upgrade straight to a Glimmer component, which doesn't have a wrapper div, you need to do something slightly different.

First, let's update the class:

app/components/flash-message.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class FlashMessageComponent extends Component {
  @tracked shouldHide = false,

  @action
  dismissMessage() {
    this.shouldHide = true;
  }
}

And now let's tweak the template:

app/components/flash-message.hbs
<div hidden={{this.shouldHide}}>
  <p>You received a message: "{{@message}}"</p>

  <button type="button" {{on 'click' this.dismissMessage}}>Dismiss</button>
</div>

As you can see, we added a wrapper div so we could use the hidden HTML attribute. We also switched from using {{action}} to using {{on}}.

§ {{partial}}

until: 4.0.0
id: ember.partial

We are deprecating usage of {{partial}} in accordance with RFC #449.

Partials should be migrated to components. For example, consider the following quick-tip partial:

app/templates/application.hbs
{{#let (hash title="Don't use partials" body="Components are always better") as |tip|}}
  {{partial "partials/quick-tip"}}
{{/let}}
app/templates/partials/quick-tip.hbs
<h1>Tip: {{tip.title}}</h1>
<p>{{tip.body}}</p>
<button {{on "click" this.dismissTip}}>OK</button>

It can be converted to a component as follows:

app/templates/application.hbs
{{#let (hash title="Don't use partials" body="Components are always better") as |tip|}}
  <QuickTip @tip={{tip}} @onDismiss={{this.dismissTip}} />
{{/let}}
app/templates/components/quick-tip.hbs
<h1>Tip: {{@tip.title}}</h1>
<p>{{@tip.body}}</p>
<button {{action @onDismiss}}>OK</button>

Deprecations Added in 3.16

§ Use ember-cli resolver rather than legacy globals resolver

until: 4.0.0
id: ember.globals-resolver

Over the past years we have transitioned to using Ember-CLI as the main way to compile Ember apps. The globals resolver is a holdover and primarily facilitates use of Ember without Ember-CLI.

If at all possible, it is highly recommended that you transition to using ember-cli to build your Ember applications. Most of the community already uses it and it provides many benefits including a rich addon ecosystem.

However, if you do have a custom build system, or are using Ember App Kit, you can adapt your current build tools and configuration instead of using ember-cli if you really need to.

Instead of extending from Ember.DefaultResolver or @ember/globals-resolver, extend from the ember-cli-resolver.

Then throughout your app, instead of compiling to:

App.NameOfThingTypeOfThing,

transpile to named amd strict syntax with module name of

<app-name/type-of-things/name-of-things>

which looks like this after transpilation

// import bar from 'bar';
// export default foo(bar);
define("my-app/utils/foo", ["exports", "bar"], function (exports, bar) {
  "use strict";

  exports.__esModule = true;

  exports["default"] = foo(bar);
});

Also, instead of including your templates in index.html, precompile your templates using the precompiler that is included with the version of Ember.js you intend to use it with. This can be found in the ember-source package under dist/ember-template-compiler.js.

Additionally, instead of using the Ember.TEMPLATES array to lookup a template, you can import it in your code:

import layout from './template.js';

export default Ember.Component.extend({ layout });

Finally, instead of creating a global namespace

App.Utils = Ember.Namespace.create();

simply create a directory and when transpiling, include the directory name in your module name.

define('my-app/utils/...', /*...*/);

If you need additional help transitioning your globals build system, feel free to reach out to someone on the Ember Community Discord or the Discourse forum.

Deprecations Added in 3.21

§ Use Ember getter and explicitly check for undefined

until: 4.0.0
id: ember-metal.get-with-default

Deprecate support for getWithDefault in Ember's Object module (@ember/object) – both the function and the class method – because its expected behaviour is confusing to Ember developers.

  • The API will only return the default value when the value of the property retrieved is undefined. This behaviour is often overlooked when using the function where a developer might expect that null or other falsey values will also return the default value.
  • The native JavaScript Nullish Coalescing Operator ?? could be used to handle this case if we also take null as a falsey value to show the default value

Before:

import { getWithDefault } from '@ember/object';

let result = getWithDefault(obj, 'some.key', defaultValue);

After:

import { get } from '@ember/object';

let result = get(obj, 'some.key');
if (result === undefined) {
  result = defaultValue;
}

Using Nullish Coalescing Operator

We cannot codemod directly into the nullish coalescing operator since the expected behaviour of getWithDefault is to only return the default value if it is strictly undefined. The nullish coalescing operator accepts either null or undefined to show the default value.

The function getWithDefault will not return the default value if the provided value is null. The function will only return the default value for undefined:

let defaultValue = 1;
let obj = {
  nullValue: null,
  falseValue: false,
};

// Returns defaultValue 1, undefinedKey = 1
let undefinedValue = getWithDefault(obj, 'undefinedKey', defaultValue);

// Returns null, nullValue = null
let nullValue = getWithDefault(obj, 'nullValue', defaultValue);

// Returns obj's falseValue, falseValue = false
let falseValue = getWithDefault(obj, 'falseValue', defaultValue);

The nullish coalescing operator (??) will return the default value when the provided value is undefined or null:

let defaultValue = 1;
let obj = {
  nullValue: null,
  falseValue: false,
};

// Returns defaultValue 1, undefinedKey = 1
let undefinedValue = get(obj, 'undefinedKey') ?? defaultValue;

// Returns defaultValue 1, nullValue = 1
let nullValue = get(obj, 'nullValue') ?? defaultValue;

// Returns obj's falseValue, falseValue = false
let falseValue = get(obj, 'falseValue') ?? defaultValue;

For any given usage of getWithDefault, using nullish coalescing might work very well, but keep in mind that either null or undefined will return the default value.

Please review the deprecation RFC over at emberjs/rfcs for more details.

§ Meta Destruction APIs

until: 3.25.0
id: meta-destruction-apis

We are deprecated usage of Ember.meta destruction apis.

  • setSourceDestroying()
  • setSourceDestroyed()
  • isSourceDestroying()
  • isSourceDestroyed()

Instead, you should use the similarly named APIs from @ember/destroyable.

RFC: https://emberjs.github.io/rfcs/0580-destroyables.html

import { destroy, isDestroying, isDestroyed } from '@ember/destroyable' ;

let component = EmberObject.create();

isDestroying(component); // => false
isDestroyed(component); // => false

destroy(component);

isDestroying(component); // => true
isDestroyed(component); // => false

// some time later
isDestroyed(component); // => true

Deprecations Added in 3.24

§ Without for

until: 4.0.0
id: ember-source-deprecation-without-for

The deprecate function now requires passing the for option to provide a namespace for the deprecation. Before:

import { deprecate } from '@ember/debug';

deprecate(
  'Please update from the bad function `somethingBad` to a better one',
  false,
  {
    id: 'get-rid-of-somethingBad',
    until: 'v4.0.0',
  }
);

After:

import { deprecate } from '@ember/debug';

deprecate(
  'Please update from the bad function `somethingBad` to a better one',
  false,
  {
    id: 'get-rid-of-somethingBad',
    until: 'v4.0.0',
    for: 'my-app',
  }
);

§ Without since

until: 4.0.0
id: ember-source-deprecation-without-since

The deprecate function now requires passing the since option to indicate when the deprecation was introduced. Before:

import { deprecate } from '@ember/debug';

deprecate(
  'Please update from the bad function `somethingBad` to a better one',
  false,
  {
    id: 'get-rid-of-somethingBad',
    until: 'v4.0.0',
  }
);

After:

import { deprecate } from '@ember/debug';

deprecate(
  'Please update from the bad function `somethingBad` to a better one',
  false,
  {
    id: 'get-rid-of-somethingBad',
    until: 'v4.0.0',
    since: 'v3.24.0',
  }
);

§ @ember/string#loc and {{loc}}

until: 4.0.0
id: ember-string.loc

Ember provides a very basic localization method via the @ember/string package loc function, and the related {{loc}} template helper.

This feature was introduced a long time ago but is insufficient for most use cases. We suggest you replace it with an addon in the Internationalization category of Ember Observer.

A popular addon that supports ICU (International Components for Unicode) message syntax and native browser Intl is ember-intl. Check the documentation for more detailed information.

§ String prototype extensions

until: 4.0.0
id: ember-string.prototype-extensions

Calling one of the Ember String methods (camelize, capitalize, classify, dasherize, decamelize, htmlSafe, underscore) directly on a string is deprecated.

While Ember addons (ember addon …) have prototype extensions disabled by default, they are enabled for applications (ember new …) making you able to call "Tomster".dasherize(), for example. Instead of calling the method on the string, you should instead import the function from @ember/string.

Before:

let mascot = "Empress Zoey";

mascot.camelize();   //=> "empressZoey"
mascot.capitalize(); //=> "Empress Zoey"
mascot.classify();   //=> "EmpressZoey"
mascot.decamelize(); //=> "empress zoey"
mascot.htmlSafe();   //=> { string: "Empress Zoey" }
mascot.underscore(); //=> "empress_zoey"
mascot.w();          //=> [ "Empress", "Zoey" ]

After:

import  {
  camelize,
  capitalize, 
  classify, 
  decamelize,
  underscore, 
  w, 
} from "@ember/string";
import { htmlSafe } from '@ember/template';

let mascot = "Empress Zoey";

camelize(mascot);   //=> "empressZoey"
capitalize(mascot); //=> "Empress Zoey"
classify(mascot);   //=> "EmpressZoey"
decamelize(mascot); //=> "empress zoey"
htmlSafe(mascot);   //=> { string: "Empress Zoey" }
underscore(mascot); //=> "empress_zoey"
w(mascot);          //=> [ "Empress", "Zoey" ]

You may also instead rely on methods from another library like lodash. Keep in mind that different libraries will behave in slightly different ways, so make sure any critical String transformations are thoroughly tested.

You can also disable String prototype extensions by editing your environment file:

// config/environment.js
ENV = {
  EmberENV: {
    EXTEND_PROTOTYPES: {
      Date: false,
      String: false,
    }
  }
}

§ tryInvoke from @ember/utils

until: 4.0.0
id: ember-utils.try-invoke

tryInvoke from the @ember/utils package is now deprecated.

In most cases, function arguments should not be optional, but in the rare occasion that an argument is optional by design, we can replace tryInvoke with JavaScript's optional chaining.

Before:

import { tryInvoke } from '@ember/utils';

foo() {
 tryInvoke(this.args, 'bar', ['baz']);
}

After:

foo() {
 this.args.bar?.('baz');
}

Deprecations Added in 3.25

§ Importing htmlSafe and isHTMLSafe from @ember/string

until: 4.0.0
id: ember-string.htmlsafe-ishtmlsafe

Importing htmlSafe and isHTMLSafe from @ember/string is deprecated.

You should instead import these functions from @ember/template.

Before:

import { htmlSafe, isHTMLSafe } from '@ember/string';

let htmlString = "<h1>Hamsters are the best!</h1>";
isHTMLSafe(htmlString); //=> false

let htmlSafeString = htmlSafe(htmlString);
isHTMLSafe(htmlSafeString); //=> true

After:

import { htmlSafe, isHTMLSafe } from '@ember/template';

let htmlString = "<h1>Hamsters are the best!</h1>";
isHTMLSafe(htmlString); //=> false

let htmlSafeString = htmlSafe(htmlString);
isHTMLSafe(htmlSafeString); //=> true

Deprecations Added in 3.26

§ Browser Support Policy

until: 4.0.0
id: 3-0-browser-support-policy

Ember's browser support policy is changing in v4.0. We will no longer support IE11, and instead will have a new support matrix including the following browsers:

  • Google Chrome
  • Mozilla Firefox
  • Microsoft Edge
  • Safari

To see more details about the policy and which versions of these browsers are supported, see the new documentation. To prepare for this, you should remove IE 11 from the list of browsers in your targets, and update it to match your organization's support policy.

§ Array Observers

until: 4.0.0
id: array-observers

Array observers are a special type of observer that can be used to synchronously react to changes in an EmberArray. In general, to refactor away from them, these reactions need to be converted from eager, synchronous reactions to lazy reactions that occur when the array in question is used or accessed.

For example, let's say that we had a class that wrapped an EmberArray and converted its contents into strings by calling toString() on them. This class could be implemented using array observers like so:

class ToStringArray {
  constructor(innerArray) {
    this._inner = innerArray;

    this._content = innerArray.map((value) => value.toString());

    innerArray.addArrayObserver(this, {
      willChange: '_innerWillChange',
      didChange: '_innerDidChange',
    });
  }

  // no-op
  _innerWillChange() {}
  _innerDidChange(innerArray, changeStart, removeCount, addCount) {
    if (removeCount) {
      // if items were removed, remove them
      this._content.removeAt(changeStart, removeCount);
    } else {
      // else, find the new items, convert them, and add them to the array
      let newItems = innerArray.slice(changeStart, addCount);

      this._content.replace(
        changeStart,
        0,
        newItems.map((value) => value.toString())
      );
    }

    // Let observers/computeds know that the value has changed
    notifyPropertyChange(this, '[]');
  }

  objectAt(index) {
    return this._content.objectAt(index);
  }
}

To move away from array observers, we could instead convert the behavior so that the objects are converted into strings when the array is accessed using objectAt. We can call this behavior lazy wrapping, as opposed to eager wrapping which happens when the item is added to the array. We can do this using the using the @cached decorator from tracked-toolbox.

import { cached } from 'tracked-toolbox';

class ToStringArray {
  constructor(innerArray) {
    this._inner = innerArray;
  }

  @cached
  get _content() {
    return this._inner.map((value) => value.toString());
  }

  objectAt(index) {
    return this._content.objectAt(index);
  }
}

This can also be accomplished with native Proxy. Your users can interact with the array using standard array syntax instead of objectAt:

class ToStringArrayHandler {
  constructor(innerArray) {
    this._inner = innerArray;
  }

  @cached
  get _content() {
    return this._inner.map((value) => value.toString());
  }

  get(target, prop) {
    return this._content.objectAt(prop);
  }
}

function createToStringArray(innerArray) {
  return new Proxy([], new ToStringArrayHandler(innerArray));
}

This solution will work with autotracking in general, since users who access the array via objectAt will be accessing the tracked property. However, it will not integrate with computed property dependencies. If that is needed, then you can instead extend Ember's built-in ArrayProxy class, which handles forwarding events and dependencies.

import ArrayProxy from '@ember/array/proxy';
import { cached } from 'tracked-toolbox';

class ToStringArray extends ArrayProxy {
  @cached
  get _content() {
    return this.content.map((value) => value.toString());
  }

  objectAtContent(index) {
    return this._content.objectAt(index);
  }
}

Converting code that watches arrays for changes

Array observers and change events can be used to watch arrays and react to changes in other ways as well. For instance, you may have a component like ember-collection that used array observers to trigger a rerender and rearrange its own representation of the array. A simplified version of this logic looks like the following:

export default Component.extend({
  layout: layout,

  init() {
    this._cells = A();
  },

  _needsRevalidate() {
    if (this.isDestroyed || this.isDestroying) {
      return;
    }
    this.rerender();
  },

  didReceiveAttrs() {
    this._super();

    this.updateItems();
  },

  updateItems() {
    var rawItems = this.get('items');

    if (this._rawItems !== rawItems) {
      if (this._items && this._items.removeArrayObserver) {
        this._items.removeArrayObserver(this, {
          willChange: noop,
          didChange: '_needsRevalidate',
        });
      }
      this._rawItems = rawItems;
      var items = A(rawItems);
      this.set('_items', items);

      if (items && items.addArrayObserver) {
        items.addArrayObserver(this, {
          willChange: noop,
          didChange: '_needsRevalidate',
        });
      }
    }
  },

  willRender() {
    this.updateCells();
  },

  updateCells() {
    // ...
  },

  actions: {
    scrollChange(scrollLeft, scrollTop) {
      // ...
      if (scrollLeft !== this._scrollLeft || scrollTop !== this._scrollTop) {
        set(this, '_scrollLeft', scrollLeft);
        set(this, '_scrollTop', scrollTop);
        this._needsRevalidate();
      }
    },
    clientSizeChange(clientWidth, clientHeight) {
      if (
        this._clientWidth !== clientWidth ||
        this._clientHeight !== clientHeight
      ) {
        set(this, '_clientWidth', clientWidth);
        set(this, '_clientHeight', clientHeight);
        this._needsRevalidate();
      }
    },
  },
});

We can refactor this to update the cells when they are accessed. We'll do this by calling updateCells within a computed property that depends on the items array:

export default Component.extend({
  layout: layout,

  init() {
    this._cells = A();
  },

  cells: computed('items.[]', function() {
    this.updateCells();

    return this._cells;
  }),

  updateCells() {
    // ...
  },

  actions: {
    scrollChange(scrollLeft, scrollTop) {
      // ...
      if (scrollLeft !== this._scrollLeft ||
          scrollTop !== this._scrollTop) {
        set(this, '_scrollLeft', scrollLeft);
        set(this, '_scrollTop', scrollTop);
        this.notifyPropertyChange('cells');
      }
    },
    clientSizeChange(clientWidth, clientHeight) {
      if (this._clientWidth !== clientWidth ||
          this._clientHeight !== clientHeight) {
        set(this, '_clientWidth', clientWidth);
        set(this, '_clientHeight', clientHeight);
        this.notifyPropertyChange('cells');
      }
    }
  }
});

Mutating untracked local state like this is generally ok as long as the local state is only a cached representation of the value that the computed or getter is deriving in general. It allows you to do things like compare the previous state to the current state during the update, and cache portions of the computation so that you do not need to redo all of it.

It is also possible that you have some code that must run whenever the array has changed, and must run eagerly. For instance, the array fragment from ember-data-model-fragments has some logic for signalling to the parent record that it has changed, which looks like this (simplified):

const StatefulArray = ArrayProxy.extend(Copyable, {
  content: computed(function () {
    return A();
  }),

  // ...

  arrayContentDidChange() {
    this._super(...arguments);

    let record = get(this, 'owner');
    let key = get(this, 'name');

    // Any change to the size of the fragment array means a potential state change
    if (get(this, 'hasDirtyAttributes')) {
      fragmentDidDirty(record, key, this);
    } else {
      fragmentDidReset(record, key);
    }
  },
});

Ideally the dirty state would be converted into derived state that could read the array it depended on. If that's not an option or would require major refactors, it is also possible to override the mutator method of the array and trigger the change when it is called. In EmberArray's, the primary mutator method is the replace() method.

const StatefulArray = ArrayProxy.extend(Copyable, {
  content: computed(function () {
    return A();
  }),

  // ...

  replace() {
    this._super(...arguments);

    let record = get(this, 'owner');
    let key = get(this, 'name');

    // Any change to the size of the fragment array means a potential state change
    if (get(this, 'hasDirtyAttributes')) {
      fragmentDidDirty(record, key, this);
    } else {
      fragmentDidReset(record, key);
    }
  },
});

Note that this method will work for arrays and array proxies that are mutated directly, but will not work for array proxies that wrap other arrays and watch changes on them. In those cases, the recommendation is to refactor such that:

  1. Changes are always intercepted by the proxy, and can call the code synchronously when they occur.
  2. The change logic is added by intercepting changes on the original array, so it will occur whenever the array changes.
  3. The API that must be called synchronously is instead driven by derived state. For instance, in the example above, the record's dirty state could be driven by the various child fragments it contains. The dirty state could be updated whenever the user accesses it, rather than by sending events such as didDirty and didReset.

Converting code that uses the willChange functionality

In general, it is no longer possible to react to an array change before it occurs except by overriding the mutation methods on the array itself. You can do this by replacing them and calling your logic before calling super.

const ArrayWithWillChange = EmberObject.extend(MutableArray, {
  replace() {
    // Your logic here

    this._super(...arguments);
  },
});

In cases where this is not possible, you can instead convert to derived state, and cache the previous value of the array to compare it the next time the state is accessed.

§ Accessing named args via {{attrs}}

until: 4.0.0
id: attrs-arg-access

The {{attrs}} object was an alternative way to reference named arguments in templates that was introduced prior to named arguments syntax being finalized. References to properties on {{attrs}} can be converted directly to named argument syntax.

Before:

{{attrs.foo}}
{{this.attrs.foo.bar}}
{{deeply (nested attrs.foobar.baz)}}

After:

 {{@foo}}
 {{@foo.bar}}
 {{deeply (nested @foobar.baz)}}

§ classBinding and classNameBindings as args in templates

until: 4.0.0
id: class-binding-and-class-name-bindings-in-templates

classBinding and classNameBindings can currently be passed as arguments to components that are invoked with curly invocation. These allow users to conditionally bind values to the class argument using a microsyntax similar to the one that can be defined in a Classic component's class body:

import Component from '@ember/component';

export default Component.extend({
  classNameBindings: ['isValid:is-valid:is-invalid']
});
{{my-component classNameBindings="isValid:is-valid:is-invalid"}}

Each binding is a triplet separated by colons. The first identifier in the triplet is the value that the class name should be bound to, the second identifier is the name of the class to add if the bound value is truthy, and the third value is the name to bind if the value is falsy.

These bindings are additive - they add to the existing bindings that are on the class, rather than replacing them. Multiple bindings can also be passed in by separating them with a space:

{{my-component
  classBinding="foo:bar"
  classNameBindings="some.boundProperty isValid:is-valid:is-invalid"
}}

These bindings can be converted into passing a concatenated string into the class argument of the component, using inline if to reproduce the same behavior. This is most conveniently done by converting the component to use angle-bracket invocation at the same time.

Before:

{{my-component
  classBinding="foo:bar"
  classNameBindings="some.boundProperty isValid:is-valid:is-invalid"
}}

After:

<MyComponent
  class="
    {{if this.foo "bar"}}
    {{if this.some.boundProperty "bound-property"}}
    {{if this.isValid "is-valid" "is-invalid"}}
  "
>

Note that we are passing in the class attribute, not the class argument. In most cases, this should work exactly the same as previously. If you referenced the class argument inside of your component, however, you will need to pass @class instead.

If you do not want to convert to angle bracket syntax for some reason, the same thing can be accomplished with the (concat) helper in curly invocation.

{{my-component
  class=(concat
    (if this.foo "bar")
    " "
    (if this.some.boundProperty "bound-property")
    " "
    (if this.isValid "is-valid" "is-invalid")
  )
}}

§ Edition: Classic

until: 4.0.0
id: editions.classic

The edition of Ember prior to Ember Octane is known as Ember Classic. This edition has been deprecated, which means that users must update to Ember Octane. To do this, you must:

  • Flip the appropriate optional feature flags for Octane:
  • application-template-wrapper: false
  • template-only-glimmer-components: true
  • Set the edition in the application's package.json to "octane":
{
  "ember": {
    "edition": "octane"
  }
}

For more details on how to upgrade to Octane, see the official upgrade guide. You can also run npx @ember/octanify, which will attempt to update these values automatically.

until: 4.0.0
id: ember-glimmer.link-to.positional-arguments

Invoking the <LinkTo> component with positional arguments is deprecated.

See below how to migrate different usages of the component.

Inline form

Before:

{{link-to "About Us" "about"}}
          ~~~~~~~~~~~~~~~~~~

Invoking the `<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`) and pass a
block for the link's content.

After:

<LinkTo @route="about">About Us</LinkTo>

Block form

Before:

{{#link-to "about"}}About Us{{/link-to}}
           ~~~~~~~

Invoking the `<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`).

After:

<LinkTo @route="about">About Us</LinkTo>

Block form with single model

Before:

{{#link-to "post" @post}}Read {{@post.title}}...{{/link-to}}
           ~~~~~~~~~~~~

Invoking the `<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`, `@model`).

After:

<LinkTo @route="post" @model={{@post}}>Read {{@post.title}}...</LinkTo>

Block form with multiple models

Before:

{{#link-to "post.comment" @comment.post @comment}}
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  Comment by {{@comment.author.name}} on {{@comment.date}}
{{/link-to}}

Invoking the `<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`, `@models`).

After:

<LinkTo @route="post.comment" @models={{array post comment}}>
  Comment by {{comment.author.name}} on {{comment.date}}
</LinkTo>

Query params

Before:

{{#link-to "posts" (query-params direction="desc" showArchived=false)}}
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  Recent Posts
{{/link-to}}

Invoking the `<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`, `@query`) and the
`hash` helper.

After:

<LinkTo @route="posts" @query={{hash direction="desc" showArchived=false}}>
  Recent Posts
</LinkTo>

§ {{with}} helper

until: 4.0.0
id: ember-glimmer.with-syntax

The use of {{with}} has been deprecated. You should replace it with either {{let}} or a combination of {{let}}, {{if}} and {{else}}:

If you always want the block to render, replace {{with}} with {{let}} directly:

Before:

{{#with (hash name="Ben" age=4) as |person|}}
  Hi {{person.name}}, you are {{person.age}} years old.
{{/with}}

After:

{{#let (hash name="Ben" age=4) as |person|}}
  Hi {{person.name}}, you are {{person.age}} years old.
{{/let}}

If you want to render a block conditionally, use a combination of {{let}} and {{if}}:

Before:

{{#with user.posts as |blogPosts|}}
  There are {{blogPosts.length}} blog posts
{{/with}}

After:

{{#let user.posts as |blogPosts|}}
  {{#if blogPosts}}
    There are {{blogPosts.length}} blog posts
  {{/if}}
{{/let}}

If you want to render a block conditionally, and otherwise render an alternative block, use a combination of {{let}}, {{if}} and {{else}}:

Before:

{{#with user.posts as |blogPosts|}}
  There are {{blogPosts.length}} blog posts
{{else}}
  There are no blog posts
{{/with}}

After:

{{#let user.posts as |blogPosts|}}
  {{#if blogPosts}}
    There are {{blogPosts.length}} blog posts
  {{else}}
    There are no blog posts
  {{/if}}
{{/let}}

§ {{hasBlock}} and {{hasBlockParams}}

until: 4.0.0
id: has-block-and-has-block-params

{{hasBlock}}

The {{hasBlock}} property is true if the component was given a default block, and false otherwise. To transition away from it, you can use the (has-block) helper instead.

{{hasBlock}}

{{! becomes }}
{{has-block}}

Unlike {{hasBlock}}, the (has-block) helper must be called, so in nested positions you will need to add parentheses around it:

{{#if hasBlock}}

{{/if}}


{{! becomes }}
{{#if (has-block)}}

{{/if}}

You may optionally pass a name to (has-block), the name of the block to check. The name corresponding to the block that {{hasBlock}} represents is "default". Calling (has-block) without any arguments is equivalent to calling (has-block "default").

{{hasBlockParams}}

The {{hasBlockParams}} property is true if the component was given a default block that accepts block params, and false otherwise. To transition away from it, you can use the (has-block-params) helper instead.

{{hasBlockParams}}

{{! becomes }}
{{has-block-params}}

Unlike {{hasBlockParams}}, the (has-block-params) helper must be called, so in nested positions you will need to add parentheses around it:

{{#if hasBlockParams}}

{{/if}}


{{! becomes }}
{{#if (has-block-params)}}

{{/if}}

You may optionally pass a name to (has-block-params), the name of the block to check. The name corresponding to the block that {{hasBlockParams}} represents is "default". Calling (has-block-params) without any arguments is equivalent to calling (has-block-params "default").

§ Implicit Injections

until: 4.0.0
id: implicit-injections

Implicit injections are injections that are made by telling Ember to inject a service (or another type of value) into every instance of a specific type of object. A common example of this was the store property that was injected into routes and controllers when users installed Ember Data by default.

export default class ApplicationRoute extends Route {
  model() {
    return this.store.findQuery('user', 123);
  }
}

Notice how the user can access this.store without having declared the store service using the @service decorator. This was accomplished by using the owner.inject API, usually in an initializer:

export default {
  initialize(app) {
    app.inject('route', 'store', 'service:store');
    app.inject('controller', 'store', 'service:store');
  }
}

Implicit injections are difficult to understand, both because it's not obvious that they exist, or where they come from.

In general, in order to migrate away from this pattern, you should use an explicit injection instead of an implicit one. You can do this by using the @service decorator wherever you are using the implicit injection currently.

Before:

import Route from '@ember/routing/route';

export default class ApplicationRoute extends Route {
  model() {
    return this.store.findQuery('user', 123);
  }
}

After:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class ApplicationRoute extends Route {
  @service store;

  model() {
    return this.store.findQuery('user', 123);
  }
}

In some cases, you may be using an injected value which is not a service. Injections of non-service values do not have a direct explicit-injection equivalent. As such, to migrate away from these, you will have to rewrite the injection to use services instead.

Before:

// app/initializers/logger.js
import EmberObject from '@ember/object';

export function initialize(application) {
  let Logger = EmberObject.extend({
    log(m) {
      console.log(m);
    }
  });

  application.register('logger:main', Logger);
  application.inject('route', 'logger', 'logger:main');
}

export default {
  name: 'logger',
  initialize: initialize
};
// app/routes/application.js
export default class ApplicationRoute extends Route {
  model() {
    this.logger.log('fetching application model...');
    //...
  }
}

After:

// app/services/logger.js
import Service from '@ember/service';

export class Logger extends Service {
  log(m) {
    console.log(m);
  }
}
// app/routes/application.js
import { inject as service } from '@ember/service';

export default class ApplicationRoute extends Route {
  @service logger;

  model() {
    this.logger.log('fetching application model...');
    //...
  }
}

In cases where it is not possible to convert a custom injection type into a service, the value can be accessed by looking it up directly on the container instead using the lookup method:

// app/routes/application.js
import { getOwner } from '@ember/application';
import { inject as service } from '@ember/service';

export default class ApplicationRoute extends Route {
  get logger() {
    if (this._logger === undefined) {
      this._logger = getOwner(this).lookup('logger:main');
    }

    return this._logger;
  }

  set logger(value) {
    this._logger = value;
  }

  model() {
    this.logger.log('fetching application model...');
    //...
  }
}

You should always include a setter until the implicit injection is removed, since the container will still attempt to pass it into the class on creation, and it will cause errors if it attempts to overwrite a value without a setter.

§ 3.4 Component Manager Capabilities

until: 4.0.0
id: manager-capabilities.components-3-4

Any component managers using the 3.4 capabilities should update to the most recent component capabilities that are available, currently 3.13. In 3.13, the only major change is that update hooks are no longer called by default. If you need update hooks, use the updateHook capability:

capabilities({
  updateHook: true,
});

§ 3.13 Modifier Manager Capabilities

until: 4.0.0
id: manager-capabilities.modifiers-3-13

Any modifier managers using the 3.13 capabilities should update to the most recent modifier capabilities, currently 3.22. In 3.22, the major changes are:

  1. The modifier definition, associated via setModifierManager is passed directly to create, rather than a factory wrapper class. Previously, you would access the class via the class property on the factory wrapper:
   // before
   class CustomModifierManager {
     capabilities = capabilities('3.13');

     createModifier(Definition, args) {
       return new Definition.class(args);
     }
   }

This can be updated to use the definition directly:

   // after
   class CustomModifierManager {
     capabilities = capabilities('3.22');

     createModifier(Definition, args) {
       return new Definition(args);
     }
   }
  1. Args are both lazy and autotracked by default. This means that in order to track an argument value, you must actually use it in your modifier. If you do not, the modifier will not update when the value changes.

    If you still need the modifier to update whenever a value changes, even if it was not used, you can manually access every value in the modifiers installModifier and updateModifier lifecycle hooks:

   function consumeArgs(args) {
     for (let key in args.named) {
       // consume value
       args.named[key];
     }

     for (let i = 0; i < args.positional.length; i++) {
       // consume value
       args.positional[i];
     }
   }

   class CustomModifierManager {
     capabilities = capabilities('3.22');

     installModifier(bucket, element, args) {
       consumeArgs(args);

       // ...
     }

     updateModifier(bucket, args) {
       consumeArgs(args);

       // ...
     }
   }

In general this should be avoided, however, and users who are writing modifiers should instead use the value if they want it to be tracked by the modifier.

§ Optional Feature: application-template-wrapper

until: 4.0.0
id: optional-feature.application-template-wrapper

Setting the application-template-wrapper optional feature to true has been deprecated. You must set this feature to false, disabling the application wrapper. For more details on this optional feature, including the changes in behavior disabling it causes and how you can disable it, see the optional features section of the Ember guides. You can also run npx @ember/octanify to set this feature to the correct value.

§ Optional Feature: jquery-integration

until: 4.0.0
id: optional-feature.jquery-integration

Setting the jquery-integration optional feature to true has been deprecated. You must set this feature to false, disabling jQuery integration. This only disables integration with Ember, jQuery can still be included and used as an independent library via the @ember/jquery addon.

For more details on this optional feature, including the changes in behavior disabling it causes and how you can disable it, see the optional features section of the Ember guides.

§ Optional Feature: template-only-glimmer-components

until: 4.0.0
id: optional-feature.template-only-glimmer-components

Setting the template-only-glimmer-components optional feature to false has been deprecated. You must set this feature to true, enabling the template-only Glimmer components. For more details on this optional feature, including the changes in behavior enabling it causes and how you can enable it, see the optional features section of the Ember guides. You can also run npx @ember/octanify to set this feature to the correct value.

§ Transition methods of routes and controllers

until: 5.0.0
id: routing.transition-methods

The following methods are deprecated:

  • transitionTo on Route
  • replaceWith on Route
  • transitionToRoute on Controller
  • replaceRoute on Controller

Instead, the user should inject the router service in the respective class and use its methods.

Route example

Before:

// app/routes/settings.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class SettingsRoute extends Route {
  @service session;

  beforeModel() {
    if (!this.session.isAuthenticated) {
      this.transitionTo('login');
    }
  }
}

After:

// app/routes/settings.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class SettingsRoute extends Route {
  @service router;
  @service session;

  beforeModel() {
    if (!this.session.isAuthenticated) {
      this.router.transitionTo('login');
    }
  }
}

Controller example

Before:

// app/controllers/new-post.js
import Controller from '@ember/controller';

export default class NewPostController extends Controller {
  @action
  async save({ title, text }) {
    let post = this.store.createRecord('post', { title, text });
    await post.save();
    return this.transitionToRoute('post', post.id);
  }
}

After:

// app/controllers/new-post.js

import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default class NewPostController extends Controller {
  @service router;

  @action
  async save({ title, text }) {
    let post = this.store.createRecord('post', { title, text });
    await post.save();
    return this.router.transitionTo('post', post.id);
  }
}

§ Property Fallback Lookup

until: 4.0.0
id: this-property-fallback

It is currently possible to reference properties on a component without a preceding this. For instance, this component:

export class MyComponent extends Component {
  name = 'Tomster';
}
Hello, {{name}}!

Would render the following HTML:

Hello, Tomster!

This style of looking up properties is known as "property fallback", and has the potential to collide with other types of values. For instance, if there was a component or helper named name, it would be rendered instead of the property. The alternative way to lookup properties is with a preceding this:

Hello, {{this.name}}!

This style does not have any ambiguity, since it's clear that we're looking up the property on this instance of the component, and not the global helper/component. Property fallback has been deprecated in favor of this style in general.

Note that property fallback can occur anywhere that a property can be referenced. Here are some examples of properties referenced using property fallback:

{{someProp}}
{{my-helper someProp}}
{{if (my-other-helper someProp)}}
{{some.nested.prop}}
<MyComponent @arg={{someProp}} @arg2={{some.nested.prop}} />

And here are the same property lookups updated to use this:

{{this.someProp}}
{{my-helper this.someProp}}
{{if (my-other-helper this.someProp)}}
{{this.some.nested.prop}}
<MyComponent @arg={{this.someProp}} @arg2={{this.some.nested.prop}} />

Deprecations Added in 3.27

§ Invoking Helpers Without Arguments and Parentheses In Named Argument Positions

until: 4.0.0
id: argument-less-helper-paren-less-invocation

With contextual helpers arriving in Ember, helpers, modifiers and components can increasingly be thought of as first-class variables that can be passed around.

Invoking a helper without arguments or parentheses in named argument positions can be ambigious and conflicts with this mental model:

<SomeComponent @arg={{someHelper}} />

In this case, it's ambigious between passing someHelper as a value to the component to be invoked later or invoking the helper with no arguments and passing the result into the component.

The current behavior is to invoke the helper with no arguments and pass in the result, but this is counterintuitive in light of the broader "helper as a value" mental model. Therefore, this invocation style is deprecated in favor of explicitly invoking the helper with parentheses:

<SomeComponent @arg={{(someHelper)}} />

Note that this is only required in this specific scenario, where:

  1. This is not in a strict mode context, AND
  2. someHelper is a global helper, i.e. not this.someHelper, @someHelper or a local variable ({{#... as |someHelper|}}), AND
  3. No arguments are provided to the helper, AND
  4. It's in an angle bracket component invocation's named argument position, i.e. not <div id={{someHelper}}> or <Foo bar={{someHelper}}> or {{foo bar=(someHelper)}}, AND
  5. Not parenthesized, i.e. not @foo={{(helper)}}, AND
  6. Not interpolated, i.e. not @foo="{{helper}}".

In pratice, this is quite rare, as it is rather uncommon for helpers to not take any arguments.

§ Run loop and computed dot access

until: 4.0.0
id: deprecated-run-loop-and-computed-dot-access

Using . to access computed or run loop functions has been deprecated, such as computed.filter. Instead, import the value directly from the module:

import { filter } from '@ember/object/computed';

Here is the complete list of deprecated functions from computed:

computed.alias, computed.and, computed.bool, computed.collect, computed.deprecatingAlias, computed.empty, computed.equal, computed.filterBy, computed.filter, computed.gte, computed.gt, computed.intersect, computed.lte, computed.lt, computed.mapBy, computed.map, computed.match, computed.max, computed.min, computed.none, computed.notEmpty, computed.not, computed.oneWay, computed.or, computed.readOnly, computed.setDiff, computed.sort, computed.sum, computed.union, computed.uniqBy, computed.uniq.

And here is the complete list of deprecated functions from run:

run.backburner, run.begin, run.bind, run.cancel, run.debounce, run.end, run.hasScheduledTimers, run.join, run.later, run.next, run.once, run.schedule, run.scheduleOnce, run.throttle, run.cancelTimers.

§ Importing Legacy Built-in Components

until: 4.0.0
id: ember.built-in-components.import

Historically, the implementation classes of the built-in components <Input>, <Textarea> and <LinkTo> were made available publicly. This is sometimes used to customize the appearance or behavior of these components by subclassing or reopening these classes.

Since Ember 3.27, the built-in components are no longer based on these legacy classes and the implementation details are no longer public. Therefore, these legacy classes have been deprecated and will be removed after Ember 4.0.0.

In order to ease migration for apps that have implemented custom components by subclassing these legacy classes, they will be moved to a legacy addon and remain "frozen" in there:

  • Checkbox: import { Checkbox } from '@ember/legacy-built-in-components';
  • TextField: import { TextField } from '@ember/legacy-built-in-components';
  • TextArea: import { TextArea } from '@ember/legacy-built-in-components';
  • LinkComponent: import { LinkComponent } from '@ember/legacy-built-in-components';

Before:

// app/components/my-checkbox.js

import Checkbox from '@ember/component/checkbox';
//                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Using Ember.Checkbox or importing from '@ember/component/checkbox' has been
// deprecated, install the `@ember/legacy-built-in-components` addon and use
// `import { Checkbox } from '@ember/legacy-built-in-components';` instead.

export class MyCheckbox extends Checkbox {
  // ...
}

After:

// app/components/my-checkbox.js

import { Checkbox } from '@ember/legacy-built-in-components';

export class MyCheckbox extends Checkbox {
  // ...
}

Likewise, accessing Ember.Checkbox, Ember.TextField, Ember.TextArea or Ember.LinkComponent will also trigger the same deprecation.

Note that there are a few caveats with using this legacy addon.

First, these legacy classes are considered "frozen" and will not receive any improvements or bug fixes going forward. In the future, their functionalities and API may diverge from the built-in components in Ember.

Second, the current implementation of Ember's built-in components are no longer based on these legacy classes. Therefore, reopening these classes imported from the addon will not have a any effect on the built-in components. See also the section on reopening built-in components.

The legacy addon is intended as a stopgap solution to avoid introducing hard blockers on upgrading to the latest version. It is strongly recommended that apps migrate away from the legacy patterns as soon as possible.

One alternative would be to create wrapper components that invoke the built-in components, rather than subclassing them directly.

Note that the TextSupport and TargetActionSupport mixins have also been deprecated. These mixins were used to share code among the built-in components. This was always considered a private implementation detail and the mixins were documented as private APIs. These private mixins are not available from the legacy addon and will be removed after Ember 4.0.0.

See RFC #671 and RFC #707 for more details about this change.

§ Built-in Components Legacy Arguments

until: 4.0.0
id: ember.built-in-components.legacy-arguments

As of Ember 3.27, these are the named arguments API of the built-in components:

<LinkTo>

  • @route
  • @model
  • @models
  • @query
  • @replace
  • @disabled
  • @current-when
  • @activeClass
  • @loadingClass
  • @disabledClass

<Input>

  • @type
  • @value
  • @checked
  • @insert-newline
  • @enter
  • @escape-press

<Textarea>

  • @value
  • @insert-newline
  • @enter
  • @escape-press

In order to reduce their API surfaces, all other arguments on these components have been deprecated. The arguments not enumerated above are either no longer necessary, no longer recommended or accidentally exposed private implementation details.

No Longer Necessary

HTML Attributes and DOM Events

See the dedicated section on Legacy HTML Attribute Arguments.

No Longer Recommended

Changing @tagName on <LinkTo>

Due to the classic component implementation heritage, the built-in components historically accepted a @tagName argument that allows customizing the tag name of the underlying HTML element.

This was once popular with the <LinkTo> component for adding navigation behavior to buttons, table row and other UI elements. The current consensus is that this is an anti-pattern and causes issues with assistive technologies.

In most cases, the <a> anchor HTML element should be used for navigational UI elements and styled with CSS to fit with the design requirements. Ocasionally, a button may be acceptable, in which case a custom event handler can be written using the router service and attached using the {{on}} modifier.

Other edge cases exist, but generally those solutions can be adapted to fulfill the requirements. For example, to make a table row clickable as a convenience, the primary column can be made into a link, while a click event handler is attached to the table row to redispatch the click to trigger the link.

Since this feature is no longer recommended, invoking <LinkTo> with the @tagName argument is now deprecated:

<LinkTo @tagName="div" ...>...</LinkTo>
        ~~~~~~~~~~~~~~
or

{{#link-to tagName="div" ...}}...{{/link-to}}
           ~~~~~~~~~~~~~

Passing the `@tagName` argument to <LinkTo> is deprecated. Using a <div>
element for navigation is not recommended as it creates issues with assistive
technologies. Remove this argument to use the default <a> element. In the rare
cases that call for using a different element, refactor to use the router
service inside a custom event handler instead.

As a temporary measure to maintain compatibility, when Ember detects that the @tagName argument is passed to the <LinkTo> component, it will revert that invocation to the legacy implementation while issuing the deprecation. This is intended as a stopgap measure to avoid introducing hard blockers on upgrading to the latest version. It is strongly recommended that apps migrate away from the legacy patterns as soon as possible.

Due to implementation differences, the legacy implementations may be less performant and have subtle differences in behavior, especially in edge cases around undocumented or deprecated functionalities. This temporary measure will stop working afer Ember 4.0.0.

With the ability to modify @tagName deprecated, the previously private @eventName and @preventDefault arguments on <LinkTo> are deprecated as well. These arguments were occasionally useful when the element is something other than an <a> element, but in the case of an <a> element, the default browser action is to navigate to the href via a full-page refresh. If that is not prevented, it would defeat the purpose of using the <LinkTo> component.

Similarly, the @bubbles argument is deprecated as well as stopPropagation() is not automatically called, so there is no need to pass this argument when that is the desired behavior. On the other hand, if it is desirable to stop event propagation, a custom event handler can be attached using the {{on}} modifier.

Note that while the <Input> and <Textarea> components also accepted the @tagName argument, it was never supported and its behavior is undefined. This may stop "working" at any point without warning and should not be relied upon.

Other Unsupported Arguments

Other named arguments not explicitly mentioned above are considered private implementation details. Due to the nature of classic components' arguments being set on its instance, any internal properties and methods could have been clobbered by a named argument with the same name.

Some examples include private properties like @active and @loading on <LinkTo>, @bubbles and @cancel on <Input> and <Textarea>, lifecycle hooks inherited from the classic component super class like @didRender, @willDestroy and so on.

Clobbering these internal properties and methods cause the components to behave in unexpected ways. This should be considered a bug and should not be relied upon. Any accidental difference in behavior caused by passing these unsupported named arguments may stop at any time without warning.

As a temporary measure to maintain compatibility, when Ember detects that an unknown argument is passed to a built-in component, it will revert that invocation to the legacy implementation while issuing an deprecation. This is intended as a stopgap measure to avoid introducing hard blockers on upgrading to the latest version. It is strongly recommended that apps migrate away from the legacy patterns as soon as possible. This temporary measure will stop working after Ember 4.0.0.

See RFC #671 and RFC #707 for more details about this change.

§ Built-in Components Legacy HTML Attribute Arguments

until: 4.0.0
id: ember.built-in-components.legacy-attribute-arguments

As of Ember 3.27, these are the named arguments API of the built-in components:

  • <LinkTo>
  • @route
  • @model
  • @models
  • @query
  • @replace
  • @disabled
  • @current-when
  • @activeClass
  • @loadingClass
  • @disabledClass
  • <Input>
  • @type
  • @value
  • @checked
  • @insert-newline
  • @enter
  • @escape-press
  • <Textarea>
  • @value
  • @insert-newline
  • @enter
  • @escape-press

In order to reduce their API surfaces, all other arguments on these components have been deprecated. The arguments not enumerated above are either no longer necessary, no longer recommended or accidentally exposed private implementation details.

No Longer Necessary

HTML Attributes

The built-in components historically accepted a varierty of named arguments for applying certain HTML attributes to the component's HTML element. This includes the following (may not be a complete list):

  • <LinkTo>
  • @id
  • @elementId (alias for @id)
  • @ariaRole (maps to the role HTML attribute)
  • @class
  • @classNames (deprecated, expands into the class HTML atttribute)
  • @classNameBindings (deprecated, expands to the class HTML atttribute)
  • @isVisible (deprecated, expands to the display: none inline style)
  • @rel
  • @tabindex
  • @target
  • @title
  • <Input>
  • @id
  • @elementId (alias for @id)
  • @ariaRole (maps to the role HTML attribute)
  • @class
  • @classNames (deprecated, expands into the class HTML atttribute)
  • @classNameBindings (deprecated, expands to the class HTML atttribute)
  • @isVisible (deprecated, expands to the display: none inline style)
  • @accept
  • @autocapitalize
  • @autocomplete
  • @autocorrect
  • @autofocus
  • @autosave
  • @dir
  • @disabled
  • @form
  • @formaction
  • @formenctype
  • @formmethod
  • @formnovalidate
  • @formtarget
  • @height
  • @indeterminate
  • @inputmode
  • @lang
  • @list
  • @max
  • @maxlength
  • @min
  • @minlength
  • @multiple
  • @name
  • @pattern
  • @placeholder
  • @readonly
  • @required
  • @selectionDirection
  • @size
  • @spellcheck
  • @step
  • @tabindex
  • @title
  • @width
  • <Textarea>
  • @id
  • @elementId (alias for @id)
  • @ariaRole (maps to the role HTML attribute)
  • @class
  • @classNames (deprecated, expands into the class HTML atttribute)
  • @classNameBindings (deprecated, expands to the class HTML atttribute)
  • @isVisible (deprecated, expands to the display: none inline style)
  • @autocapitalize
  • @autocomplete
  • @autocorrect
  • @autofocus
  • @cols
  • @dir
  • @disabled
  • @form
  • @lang
  • @maxlength
  • @minlength
  • @name
  • @placeholder
  • @readonly
  • @required
  • @rows
  • @selectionDirection
  • @selectionEnd
  • @selectionStart
  • @spellcheck
  • @tabindex
  • @title
  • @wrap

These arguments are no longer necessary – with angle bracket invocations, HTML attributes can be passed directly. An invocation passing one or more of these named arguments now triggers a deprecation warning.

Before:

<Input @placeholder="Ember.js" />
       ~~~~~~~~~~~~~~~~~~~~~~~
or

{{input placeholder="Ember.js"}}
        ~~~~~~~~~~~~~~~~~~~~~~

Passing the `@placeholder` argument to <Input> is deprecated. Instead, please
pass the attribute directly, i.e. `<Input placeholder={{...}} />` instead of
`<Input @placeholder={{...}} />` or `{{input placeholder=...}}`.

After:

<Input placeholder="Ember.js" />

A notable exception when passing an argument named @href to the <LinkTo> component. This was never intentionally supported and will trigger an error instead of a deprecation warning.

DOM Events

The built-in components historically accepted a variety of named arguments for listening to certain DOM events on the component's HTML element. This includes the following (may not be a complete list):

  • <LinkTo>
  • @change
  • @click
  • @contextMenu (for the contextmenu event)
  • @doubleClick (for the dblclick event)
  • @drag
  • @dragEnd (for the dragend event)
  • @dragEnter (for the dragenter event)
  • @dragLeave (for the dragleave event)
  • @dragOver (for the dragover event)
  • @dragStart (for the dragstart event)
  • @drop
  • @focusIn (for the focusin event)
  • @focusOut (for the focusout event)
  • @input
  • @keyDown (for the keydown event)
  • @keyPress (for the keypress event)
  • @keyUp (for the keyup event)
  • @mouseDown (for the mousedown event)
  • @mouseEnter (deprecated, for the mouseenter event)
  • @mouseLeave (deprecated, for the mouseleave event)
  • @mouseMove (deprecated, for the mousemove event)
  • @mouseUp (for the mouseup event)
  • @submit
  • @touchCancel (for the touchcancel event)
  • @touchEnd (for the touchend event)
  • @touchMove (for the touchmove event)
  • @touchStart (for the touchstart event)
  • <Input>
  • @click
  • @contextMenu (for the contextmenu event)
  • @doubleClick (for the dblclick event)
  • @drag
  • @dragEnd (for the dragend event)
  • @dragEnter (for the dragenter event)
  • @dragLeave (for the dragleave event)
  • @dragOver (for the dragover event)
  • @dragStart (for the dragstart event)
  • @drop
  • @input
  • @mouseDown (for the mousedown event)
  • @mouseEnter (deprecated, for the mouseenter event)
  • @mouseLeave (deprecated, for the mouseleave event)
  • @mouseMove (deprecated, for the mousemove event)
  • @mouseUp (for the mouseup event)
  • @submit
  • @touchCancel (for the touchcancel event)
  • @touchEnd (for the touchend event)
  • @touchMove (for the touchmove event)
  • @touchStart (for the touchstart event)
  • @focus-in (for the focusin event)
  • @focus-out (for the focusout event)
  • @key-down (for the keydown event)
  • @key-press (for the keypress event)
  • @key-up (for the keyup event)
  • <Textarea>
  • @click
  • @contextMenu (for the contextmenu event)
  • @doubleClick (for the dblclick event)
  • @drag
  • @dragEnd (for the dragend event)
  • @dragEnter (for the dragenter event)
  • @dragLeave (for the dragleave event)
  • @dragOver (for the dragover event)
  • @dragStart (for the dragstart event)
  • @drop
  • @input
  • @mouseDown (for the mousedown event)
  • @mouseEnter (deprecated, for the mouseenter event)
  • @mouseLeave (deprecated, for the mouseleave event)
  • @mouseMove (deprecated, for the mousemove event)
  • @mouseUp (for the mouseup event)
  • @submit
  • @touchCancel (for the touchcancel event)
  • @touchEnd (for the touchend event)
  • @touchMove (for the touchmove event)
  • @touchStart (for the touchstart event)
  • @focus-in (for the focusin event)
  • @focus-out (for the focusout event)
  • @key-down (for the keydown event)
  • @key-press (for the keypress event)
  • @key-up (for the keyup event)

These arguments are no longer necessary – with angle bracket invocations, DOM event listeners can be registered directly using the {{on}} modifier. An invocation passing one or more of these named arguments now triggers a deprecation warning.

Before:

<Input @click={{this.onClick}} />
       ~~~~~~~~~~~~~~~~~~~~~~~
or

{{input click=this.onClick}}
        ~~~~~~~~~~~~~~~~~~

Passing the `@click` argument to <Input> is deprecated. Instead, please use the
{{on}} modifier, i.e. `<Input {{on "click" ...}} />` instead of
`<Input @click={{...}} />` or `{{input click=...}}`.

After:

<Input {{on "click" this.onClick}} />

Note that these named arguments were not necessarily an intentional part of the component's original design. Rather, these are callbacks that would have fired on all classic components, and since classic components' arguments are set on the component instances as properties, passing these arguments at invocation time would have "clobbered" any callbacks with the same name defined on the component's class/prototype, whether it was intended by the component's author or not.

For instance, the <Input> and <Textarea> built-in components implemented callbacks that would have been clobbered by these named arguments (may not be a complete list):

  • @change
  • @focusIn
  • @focusOut
  • @keyDown
  • @keyPress
  • @keyUp

Passing these named arguments historically suppressed certain behavior of the built-in components, in some cases preventing the components from functioning properly. This was never an intended part of the original design and should be considered a bug.

The new implementations are generally more robust against these issues, so that passing these deprecated arguments no longer clobbers internal methods or supresses built-in functionalities. This is generally desirable and should be the expected behavior going forward.

However, apps that passes these arguments should take special care to confirm they were not inadvertently relying on the built-in functionalities being suppressed. An invocation with these named arguments now triggers a deprecation warning with this additional caveat.

Before:

<Input @change={{this.onChange}} />
       ~~~~~~~~~~~~~~~~~~~~~~~~~
or

{{input change=this.onChange}}
        ~~~~~~~~~~~~~~~~~~~~

Passing the `@change` argument to <Input> is deprecated. This would have
overwritten the internal `change` method on the <Input> component and prevented
it from functioning properly. Instead, please use the {{on}} modifier, i.e.
`<Input {{on "change" ...}} />` instead of `<Input @change={{...}} />` or
`{{input change=...}}`.

After:

<Input {{on "change" this.onChange}} />

Other Arguments

See the section on Other Legacy Arguments.

See RFC #671 and RFC #707 for more details about this change.

§ Reopening Legacy Built-in Components

until: 4.0.0
id: ember.built-in-components.reopen

Historically, the implementation classes of the built-in components <Input>, <Textarea> and <LinkTo> were made available publicly. This is sometimes used to customize the apperances or behavior of these components by subclassing or reopening these classes.

Since Ember 3.27, the built-in components are no longer based on these legacy classes and the implementation details are no longer public. After 4.0.0, it will not be possible to reopen the built-in components.

As a temporary measure to maintain compatibility, when Ember detects that a built-in component is reopened, it will revert that component to its legacy implementation while issuing a deprecation. This is intended as a stopgap measure to avoid introducing hard blockers on upgrading to the latest version. It is strongly recommended that apps migrate away from the legacy patterns as soon as possible.

Due to implementation differences, the legacy implementations may be less performant and have subtle differences in behavior, especially in edge cases around undocumented or deprecated functionailities. This temporary measure will stop working afer Ember 4.0.0.

One alternative would be to create wrapper components that invokes the built-in components, rather than subclassing them directly.

Before:

Checkbox.reopen({
//      ~~~~~~~
// Reopening Ember.Checkbox has been deprecated. Consider implementing your own
// wrapper component or create a custom subclass.
  attributeBindings: ['metadata:data-my-metadata'],
  metadata: ''
});

After:

{{!-- app/components/my-checkbox.hbs --}}

<Input
  @type="checkbox"
  @checked={{@checked}}
  ...attributes
  data-my-metadata={{@metadata}}
/>

Likewise, calling reopenClass on these built-in components will also trigger the same deprecation.

Alternatively, you may also implement your own customized version of the component installing the @ember/legacy-built-in-components addon. This addon vendors the legacy classes and make them available for subclassing.

Before:

Checkbox.reopen({
//      ~~~~~~~
// Reopening Ember.Checkbox has been deprecated. Consider implementing your own
// wrapper component or create a custom subclass.
  change(...args) {
    console.log('changed');
    this._super(...args);
  }
});

After:

// app/components/my-checkbox.js

import { Checkbox } from '@ember/legacy-built-in-components';

export default class MyCheckbox extends Checkbox {
  change(...args) {
    console.log('changed');
    super.change(...args);
  }
}

Note that this legacy addon merely makes the legacy classes available, it does not revert the built-in components' implementation to be based on these legacy classes. You cannot simply reopen the classes provided by this addon.

The legacy addon is only meant to be a stopgap solution. See the section on importing built-in components for more details.

Finally, because the legacy implementations are based on the classic component (Ember.Component or import Component from '@ember/component';) super class, reopening the classic component super class will revert all built-in components to their legacy implementations while triggering a deprecation warning. This temporary measure will stop working afer Ember 4.0.0.

Reopening a the classic component super class is dangerous and has far-reaching consequences. For example, it may unexpectedly break addons that are not expecting the changes.

To respond to DOM events globally, consider using global event listeners instead.

Before:

import Component from '@ember/component';

Component.reopen({
//       ~~~~~~~
// Reopening the Ember.Component super class itself has been deprecated. Consider
// alternatives such as installing event listeners on the document or add the
// customizations to specific subclasses.
  click() {
    console.log('Clicked on a classic component');
  }
});

After:

document.addEventListener('click', event => {
  if (e.target.classList.contains('ember-view')) {
    console.log('Clicked on a classic component');
  }
});

Alternatively, you may create a custom subclass of Ember.Component with the behavior you want and subclass from that in your app. That way, only those components which explictly opted into the changes will be affected.

Before:

import Component from '@ember/component';

Component.reopen({
//       ~~~~~~~
// Reopening the Ember.Component super class itself has been deprecated. Consider
// alternatives such as installing event listeners on the document or add the
// customizations to specific subclasses.
  attributeBindings: ['metadata:data-my-metadata'],
  metadata: ''
});

After:

// app/components/base.js

import Component from '@ember/component';

// Subclass from this in your app, instead of subclassing from Ember.Component
export default Component.extend({
  attributeBindings: ['metadata:data-my-metadata'],
  metadata: ''
});

See RFC #671 and RFC #707 for more details about this change.

§ Reopening Classic Component Super Class

until: 4.0.0
id: ember.component.reopen

Reopening the Ember.Component super class has far-reaching consequences. For example, it may unexpectedly break addons that are not expecting the changes.

To respond to DOM events globally, consider instead using global event listeners.

Before:

import Component from '@ember/component';

Component.reopen({
  click() {
    console.log('Clicked on a classic component');
  }
});

After:

document.addEventListener('click', event => {
  if (event.target.classList.contains('my-component')) {
    console.log('Clicked on a classic component');
  }
});

Alternatively, you can create a custom subclass of Ember.Component with the behavior you want and subclass from that component in your app. That way, only those components which explicitly opted into the changes will be affected.

Before:

import Component from '@ember/component';

Component.reopen({
  attributeBindings: ['metadata:data-my-metadata'],
  metadata: ''
});

After:

// app/components/base.js
import Component from '@ember/component';

// Subclass from this in your app, instead of subclassing from Ember.Component
export default Component.extend({
  attributeBindings: ['metadata:data-my-metadata'],
  metadata: ''
});

§ Deprecate the Ember Global

until: 4.0.0
id: ember-global

Accessing Ember on the global context (e.g. window.Ember, globalThis.Ember, or just Ember without importing it) is no longer supported. Migrate to importing Ember explicitly instead. See RFC 706 for more details.

Before:

export default class MyComponent extends Ember.Component {
  // ...
}

After:

import Ember from 'ember';

export default class MyComponent extends Ember.Component {
  // ...
}

Alternatively, consider converting to use the Ember modules API equivalent to the API you are using:

import Component from '@ember/component';

export default class MyComponent extends Component {
  // ...
}

If there is no modules API equivalent, consider refactoring away from using that API.

until: 4.0.0
id: ember.link-to.disabled-when

Passing @disabled-when argument to <LinkTo> component has been deprecated. You can use @disabled instead.

Before:

<LinkTo @route='photoGallery' @disabled-when={{true}}>
  Great Dragon Photos
</LinkTo>

After:

<LinkTo @route='photoGallery' @disabled={{true}}>
  Great Dragon Photos
</LinkTo>

§ Deprecate Route#disconnectOutlet

until: 4.0.0
id: route-disconnect-outlet

Route#disconnectOutlet is intended to be used in conjunction with Route#render. As render is deprecated and disconnectOutlet is primarily used to teardown named outlets setup by render, it is also deprecated. See RFC #491.

The migration path is the same as the one defined for Route#render where components should be used instead of named outlets. A developer should wrap the component in a conditional if they want to control its destruction.

Given:

// app/routes/checkout.js
class CheckoutRoute extends Route {
  // ...

  @action
  showModal() {
    this.render('modal', {
      outlet: 'modal',
      into: 'application'
    });
  }

  @action
  hideModal() {
    this.disconnectOutlet('modal');
  }
}
{{! app/templates/checkout.hbs}}
<button {{on "click" this.showModal}}>Show Modal</button>
<button {{on "click" this.closeModal}}>Close Modal</button>
{{! app/templates/application.hbs}}
{{outlet "modal"}}

<main>
  {{outlet}}
</main>

This can transitioned to:

// app/controller/checkout.js
class CheckoutController extends Controller {
  // ...
  @tracked isModalOpen = false;

  init() {
    super.init();
    this.modalElement = document.getElementById('modal');
  }

  @action
  showModal() {
    this.isModalOpen = true;
  }

  @action
  closeModal() {
    this.isModalOpen = false;
  }
}
{{! app/templates/checkout.hbs}}
<button {{on "click" this.showModal}}>Show Modal</button>
<button {{on "click" this.closeModal}}>Close Modal</button>

{{#if this.isModalOpen}}
  {{#in-element this.modalElement}}
    <Modal />
  {{/in-element}}
{{/if}}
{{! app/templates/application.hbs}}
<div id="modal"></div>

<main>
  {{outlet}}
</main>

The above example will conditionally append the modal component into div#modal whenever the user toggles the modal.

§ Deprecate Route#renderTemplate

until: 4.0.0
id: route-render-template

The Route#render and Route#renderTemplate APIs have been deprecated. These APIs are largely holdovers from a time where components where not as prominent in your typical Ember application and are no longer relevant. See RFC #418.

The migration plan here is going to be somewhat situational based on the UI that was being constructed. For cases where named outlets were being used it is likely that they should just be moved to components. For cases where you were escaping the existing DOM hierarchy to render a template somewhere else in the DOM, one should use the built-in {{in-element}} helper or an addon like ember-wormhole. Below are some example of how a migration would look.

Migrating Named Outlets

Given:

// app/routes/checkout.js
class CheckoutRoute extends Route {
  // ...
  renderTemplate() {
    this.render('cart', {
      into: 'checkout',
      outlet: 'cart',
      controller: 'cart'
    })
  }
}
{{! checkout.hbs}}
<section id="items">
  {{outlet}}
</section>
<aside>
  {{outlet "cart"}}
</aside>

This would tell Ember to render cart.hbs into checkout.hbs at the {{outlet "cart"}} and use the cart controller to back the cart.hbs template.

We can migrate this entirely to use components.

{{! checkout.hbs}}
<section id="items">
  {{outlet}}
</section>
<aside>
  <Cart />
</aside>

Migrating Hiearchy Escaping

// app/routes/checkout.js
class CheckoutRoute extends Route {
  // ...

  @action
  showModal() {
    this.render('modal', {
      outlet: 'modal',
      into: 'application'
    });
  }

  @action
  hideModal() {
    this.disconnectOutlet('modal');
  }
}
{{! app/templates/checkout.hbs}}
<button {{on "click" this.showModal}}>Show Modal</button>
<button {{on "click" this.closeModal}}>Close Modal</button>
{{! app/templates/application.hbs}}
{{outlet "modal"}}

<main>
  {{outlet}}
</main>

This can transitioned to:

// app/controller/checkout.js
class CheckoutController extends Controller {
  // ...
  @tracked isModalOpen = false;

  init() {
    super.init();
    this.modalElement = document.getElementById('modal');
  }

  @action
  showModal() {
    this.isModalOpen = true;
  }

  @action
  closeModal() {
    this.isModalOpen = false;
  }
}
{{! app/templates/checkout.hbs}}
<button {{on "click" this.showModal}}>Show Modal</button>
<button {{on "click" this.closeModal}}>Close Modal</button>

{{#if this.isModalOpen}}
  {{#in-element this.modalElement}}
    <Modal />
  {{/in-element}}
{{/if}}
{{! app/templates/application.hbs}}
<div id="modal"></div>

<main>
  {{outlet}}
</main>

The above example will conditionally append the modal component into div#modal whenever the user toggles the modal.

§ Class-based template compilation plugins

until: 4.0.0
id: template-compiler.registerPlugin

Using class-based template compilation plugins is deprecated. Please update to the functional style.

If you see this deprecation when building an app, most likely it's coming from one of the addons you have installed. You can use the class name of the plugin included in the deprecation message to figure out which addon is triggering this deprecation, like MyTemplateCompilationPlugin in the example below.

Before:

'use strict';

module.exports = class MyTemplateCompilationPlugin {
  transform(ast) {
    let visitor = {
      BlockStatement(node) {
        // ...
      },

      ElementNode(node) {
        // ...
      },

      MustacheStatement(node) {
        // ...
      },
    };

    this.syntax.traverse(ast, visitor);

    return ast;
  }
};

After:

'use strict';

module.exports = function myTemplateCompilationPlugin() {
  return {
    visitor: {
      BlockStatement(node) {
        // ...
      },

      ElementNode(node) {
        // ...
      },

      MustacheStatement(node) {
        // ...
      },
    },
  };
};

Deprecations Added in 3.28

§ Deprecate setting properties on objects generated by {{hash}}

until: 4.4.0
id: setting-on-hash

Objects generated by {{hash}} helper method no longer supports setting properties because it was defined on the original hash and is a reference to the original value. Objects generated by {{hash}} can be considered immutable as internally it returns a Proxy object rather than an original object. You can get the same functionality by using an object created with a tracked property or getter, or with a custom helper.

Before:

app/templates/application.hbs
<Greeting @person={{hash firstName='Christian' lastName='Bale'}} />
app/components/greeting.js
export default class GreetingComponent extends Component {
  constructor() {
    super(...arguments);

    const person = this.args.person;
    person.firstName = 'Bruce';
    person.lastName = 'Wayne';
  }
}
app/components/greeting.hbs
Hello, {{@person.firstName}} {{@person.lastName}}
{{!-- Hello, Christian Bale --}}

After:

app/templates/application.hbs
<Greeting @person={{this.person}} />
app/controllers/application.js
export default class ApplicationController extends Controller {
  @tracked person = {
    firstName: 'Christian',
    lastName: 'Bale',
  };
}
app/components/greeting.hbs
Hello, {{@person.firstName}} {{@person.lastName}}
{{!-- Hello, Bruce Wayne --}}

Deprecations Added in Glimmer Internals

§ Mutation After Consumption

until: 4.0.0
id: autotracking.mutation-after-consumption

Older versions of Ember failed to detect errors in certain cases where an autotracked property was both read from and written to during rendering. This was buggy and could cause infinite loops, as with all such combined read-write operations during rendering. A common case was reading from and writing to a @tracked property in a constructor, usually for making a local copy of a value from args:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class Example extends Component {
  @tracked aLocalCopy;

  constructor() {
    super(...arguments);
    if (this.args.aLocalCopy !== this.args.inboundValue) {
      this.args.aLocalCopy = this.args.inboundValue;
    }
  }

  @action updateLocal(newValue) {
    this.aLocalCopy = newValue;
  }
}

(Note that this behavior also did not have the intended effect: constructors only run once for any given component instance!)

The fix is usually to derive state instead. If you need to allow local state to diverge, you can do that with a separate tracked property. For example:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class Example extends Component {
  @tracked aLocalCopy;

  get localData() {
    return this.aLocalCopy ?? this.args.inboundValue;
  }

  @action updateLocal(newValue) {
    this.aLocalCopy = newValue;
  }
}

Deprecations Added in Upcoming Features

§ Prototype Function Listeners

until: 3.9.0
id: events.inherited-function-listeners

Currently, function listeners and string listeners behave identically to each other. Their inheritance and removal structure is the same, and they can be used interchangeably for the most part. However, function listeners can be much more expensive as they maintain a reference to the function.

Function listeners also have limited utility outside of per instance usage. Consider the following example which the same listener using strings and using function references:

class Foo {
  method() {}
}

addListener(Foo, 'event', null, 'method');
addListener(Foo, 'event', null, Foo.prototype.method);

It's clear that the string version is much more succinct and preferable. A more common alternative would be adding the listener to the instance in the constructor:

class Foo {
  constructor() {
    addListener(this, 'event', this, this.method);
  }

  method() {}
}

But in this case, the listener doesn't need to be applied to the prototype either.

Updating

In cases where function listeners have been added to a prototype, and those functions do exist on the prototype, replace them with string listeners:

Before:

class Foo {
  method() {}
}

addListener(Foo, 'event', null, Foo.prototype.method);

After:

class Foo {
  method() {}
}

addListener(Foo, 'event', null, 'method');

In cases where function listeners have been added to a prototype for arbitrary functions which do not exist on the prototype, you can convert the function to a method, create a wrapper function, or add the listener on the instance instead:

Before:

function foo() {}

class Foo {}

addListener(Foo, 'event', null, foo);

After:

class Foo {
  foo() {}
}

addListener(Foo, 'event', null, 'foo');

// OR
function originalFoo() {}

class Foo {
  foo() {
    originalFoo();
  }
}

addListener(Foo, 'event', null, 'foo');

// OR
function foo() {}

class Foo {
  constructor() {
    addListener(this, 'event', this, foo);
  }
}