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
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
Calling array.get('@each')
is deprecated. @each
may only be used as dependency key.
§ 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
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.
§
Private property Route.router
has been renamed to 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()
});
§ Use defineProperty to define computed properties
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';
Deprecations Added in 3.3
§ Use ember-copy addon instead of copy method and Copyable mixin.
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.
§ 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:
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
.
ENV = {
EmberENV: {
EXTEND_PROTOTYPES: false
}
}
§ Use native events instead of 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
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.
export default Controller.extend({
actions: {
sendData(data) {
fetch('/endpoint', { body: JSON.stringify(data) });
}
}
})
this.sendAction('submit');
Should be changed to:
export default Controller.extend({
actions: {
sendData(data) {
fetch('/endpoint', { body: JSON.stringify(data) });
}
}
})
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:
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:
Since this uses sendAction
underneath it is also deprecated and must also be replaced by closure actions:
Deprecations Added in 3.6
§ Ember.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' }
§
Calling A
as a constructor
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();
§ new EmberObject
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.
§ Remove All Listeners/Observers
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');
§ HandlerInfos Removal
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 RouteInfo
s. 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,
}
});
}
}
});
§ 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
});
});
}
});
§ Transition State Removal
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 RouteInfo
s. 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
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
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:
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:
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
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
.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
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}`;
}
});
§
@ember/object#aliasMethod
@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,
});
§ Replace 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.Component
s, 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
!
Deprecations Added in 3.11
§ Function.prototype.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
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
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
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 >Hover</button>
After:
<button >Hover</button>
§ mouseEnter/Leave/Move component methods
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 >
...
</div>
Deprecations Added in 3.15
§
Component#isVisible
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:
import Component from '@ember/component';
export default Component.extend({
isVisible: true,
dismissMessage() {
this.set('isVisible', false);
}
});
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:
import Component from '@ember/component';
export default Component.extend({
shouldShow: true,
dismissMessage() {
this.set('shouldShow', false);
}
});
Now we wrap the template in a conditional:
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:
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:
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:
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:
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}}
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:
It can be converted to a component as follows:
Deprecations Added in 3.16
§ Use ember-cli resolver rather than legacy 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
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 thatnull
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 takenull
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
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
§
@ember/string#loc
and {{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.
§ 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
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',
}
);
§ 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
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
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
§ 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:
- Changes are always intercepted by the proxy, and can call the code synchronously when they occur.
- The change logic is added by intercepting changes on the original array, so it will occur whenever the array changes.
- 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
anddidReset
.
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}}
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:
After:
§ 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.
§ classBinding and classNameBindings as args 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']
});
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:
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:
After:
<MyComponent
class="
"
>
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.
§ Edition: 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.
§ {{hasBlock}} and {{hasBlockParams}}
{{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.
Unlike {{hasBlock}}
, the (has-block)
helper must be called, so in nested
positions you will need to add parentheses around it:
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.
Unlike {{hasBlockParams}}
, the (has-block-params)
helper must be called, so in nested
positions you will need to add parentheses around it:
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
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.
§
<LinkTo>
positional arguments
Invoking the <LinkTo>
component with positional arguments is deprecated.
See below how to migrate different usages of the component.
Inline form
Before:
<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`) and pass a
block for the link's content.
~~~~~~~~~~~~~~~~~~
Invoking the `
After:
<LinkTo @route="about">About Us</LinkTo>
Block form
Before:
<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`).
About Us
~~~~~~~
Invoking the `
After:
<LinkTo @route="about">About Us</LinkTo>
Block form with single model
Before:
<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`, `@model`).
Read ...
~~~~~~~~~~~~
Invoking the `
After:
<LinkTo @route="post" @model=>Read ...</LinkTo>
Block form with multiple models
Before:
<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`, `@models`).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comment by on
Invoking the `
After:
<LinkTo @route="post.comment" @models=>
Comment by on
</LinkTo>
Query params
Before:
<LinkTo>` component with positional arguments is deprecated.
Instead, please use the equivalent named arguments (`@route`, `@query`) and the
`hash` helper.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Recent Posts
Invoking the `
After:
<LinkTo @route="posts" @query=>
Recent Posts
</LinkTo>
§ 3.4 Component Manager Capabilities
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
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:
- The modifier definition, associated via
setModifierManager
is passed directly tocreate
, rather than a factory wrapper class. Previously, you would access the class via theclass
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);
}
}
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
andupdateModifier
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
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
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
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
The following methods are deprecated:
transitionTo
onRoute
replaceWith
onRoute
transitionToRoute
onController
replaceRoute
onController
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
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,
!
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 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:
<MyComponent @arg= @arg2= />
And here are the same property lookups updated to use this
:
<MyComponent @arg= @arg2= />
§
{{with}}
helper
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:
Hi , you are years old.
After:
Hi , you are years old.
If you want to render a block conditionally, use a combination of {{let}}
and {{if}}
:
Before:
There are blog posts
After:
There are blog posts
If you want to render a block conditionally, and otherwise render an alternative block, use a combination of {{let}}
, {{if}}
and {{else}}
:
Before:
There are blog posts
There are no blog posts
After:
There are blog posts
There are no blog posts
Deprecations Added in 3.27
§ Invoking Helpers Without Arguments and Parentheses In Named Argument Positions
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= />
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= />
Note that this is only required in this specific scenario, where:
- This is not in a strict mode context, AND
someHelper
is a global helper, i.e. notthis.someHelper
,@someHelper
or a local variable ({{#... as |someHelper|}}
), AND- No arguments are provided to the helper, AND
- 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 - Not parenthesized, i.e. not
@foo={{(helper)}}
, AND - 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
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
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
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
...
~~~~~~~~~~~~~
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
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 therole
HTML attribute)@class
@classNames
(deprecated, expands into theclass
HTML atttribute)@classNameBindings
(deprecated, expands to theclass
HTML atttribute)@isVisible
(deprecated, expands to thedisplay: none
inline style)@rel
@tabindex
@target
@title
<Input>
@id
@elementId
(alias for@id
)@ariaRole
(maps to therole
HTML attribute)@class
@classNames
(deprecated, expands into theclass
HTML atttribute)@classNameBindings
(deprecated, expands to theclass
HTML atttribute)@isVisible
(deprecated, expands to thedisplay: 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 therole
HTML attribute)@class
@classNames
(deprecated, expands into theclass
HTML atttribute)@classNameBindings
(deprecated, expands to theclass
HTML atttribute)@isVisible
(deprecated, expands to thedisplay: 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
~~~~~~~~~~~~~~~~~~~~~~
Passing the `@placeholder` argument to <Input> is deprecated. Instead, please
pass the attribute directly, i.e. `<Input placeholder= />` instead of
`<Input @placeholder= />` or ` `.
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 thecontextmenu
event)@doubleClick
(for thedblclick
event)@drag
@dragEnd
(for thedragend
event)@dragEnter
(for thedragenter
event)@dragLeave
(for thedragleave
event)@dragOver
(for thedragover
event)@dragStart
(for thedragstart
event)@drop
@focusIn
(for thefocusin
event)@focusOut
(for thefocusout
event)@input
@keyDown
(for thekeydown
event)@keyPress
(for thekeypress
event)@keyUp
(for thekeyup
event)@mouseDown
(for themousedown
event)@mouseEnter
(deprecated, for themouseenter
event)@mouseLeave
(deprecated, for themouseleave
event)@mouseMove
(deprecated, for themousemove
event)@mouseUp
(for themouseup
event)@submit
@touchCancel
(for thetouchcancel
event)@touchEnd
(for thetouchend
event)@touchMove
(for thetouchmove
event)@touchStart
(for thetouchstart
event)<Input>
@click
@contextMenu
(for thecontextmenu
event)@doubleClick
(for thedblclick
event)@drag
@dragEnd
(for thedragend
event)@dragEnter
(for thedragenter
event)@dragLeave
(for thedragleave
event)@dragOver
(for thedragover
event)@dragStart
(for thedragstart
event)@drop
@input
@mouseDown
(for themousedown
event)@mouseEnter
(deprecated, for themouseenter
event)@mouseLeave
(deprecated, for themouseleave
event)@mouseMove
(deprecated, for themousemove
event)@mouseUp
(for themouseup
event)@submit
@touchCancel
(for thetouchcancel
event)@touchEnd
(for thetouchend
event)@touchMove
(for thetouchmove
event)@touchStart
(for thetouchstart
event)@focus-in
(for thefocusin
event)@focus-out
(for thefocusout
event)@key-down
(for thekeydown
event)@key-press
(for thekeypress
event)@key-up
(for thekeyup
event)<Textarea>
@click
@contextMenu
(for thecontextmenu
event)@doubleClick
(for thedblclick
event)@drag
@dragEnd
(for thedragend
event)@dragEnter
(for thedragenter
event)@dragLeave
(for thedragleave
event)@dragOver
(for thedragover
event)@dragStart
(for thedragstart
event)@drop
@input
@mouseDown
(for themousedown
event)@mouseEnter
(deprecated, for themouseenter
event)@mouseLeave
(deprecated, for themouseleave
event)@mouseMove
(deprecated, for themousemove
event)@mouseUp
(for themouseup
event)@submit
@touchCancel
(for thetouchcancel
event)@touchEnd
(for thetouchend
event)@touchMove
(for thetouchmove
event)@touchStart
(for thetouchstart
event)@focus-in
(for thefocusin
event)@focus-out
(for thefocusout
event)@key-down
(for thekeydown
event)@key-press
(for thekeypress
event)@key-up
(for thekeyup
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= />
~~~~~~~~~~~~~~~~~~~~~~~
or
~~~~~~~~~~~~~~~~~~
Passing the `@click` argument to <Input> is deprecated. Instead, please use the
modifier, i.e. `<Input />` instead of
`<Input @click= />` or ` `.
After:
<Input />
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= />
~~~~~~~~~~~~~~~~~~~~~~~~~
or
~~~~~~~~~~~~~~~~~~~~
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 modifier, i.e.
`<Input />` instead of `<Input @change= />` or
` `.
After:
<Input />
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
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:
<Input
@type="checkbox"
@checked=
...attributes
data-my-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
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
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.
§ LinkTo @disabled-when argument
Passing @disabled-when
argument to <LinkTo>
component has been deprecated. You can use @disabled
instead.
Before:
<LinkTo @route='photoGallery' @disabled-when=>
Great Dragon Photos
</LinkTo>
After:
<LinkTo @route='photoGallery' @disabled=>
Great Dragon Photos
</LinkTo>
§
Deprecate Route#disconnectOutlet
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');
}
}
<button >Show Modal</button>
<button >Close Modal</button>
<main>
</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;
}
}
<button >Show Modal</button>
<button >Close Modal</button>
<Modal />
<div id="modal"></div>
<main>
</main>
The above example will conditionally append the modal component into div#modal
whenever the user toggles the modal.
§
Deprecate Route#renderTemplate
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'
})
}
}
<section id="items">
</section>
<aside>
</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.
<section id="items">
</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');
}
}
<button >Show Modal</button>
<button >Close Modal</button>
<main>
</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;
}
}
<button >Show Modal</button>
<button >Close Modal</button>
<Modal />
<div id="modal"></div>
<main>
</main>
The above example will conditionally append the modal component into div#modal
whenever the user toggles the modal.
§ Class-based template compilation plugins
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}}
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:
export default class GreetingComponent extends Component {
constructor() {
super(...arguments);
const person = this.args.person;
person.firstName = 'Bruce';
person.lastName = 'Wayne';
}
}
After:
export default class ApplicationController extends Controller {
@tracked person = {
firstName: 'Christian',
lastName: 'Bale',
};
}
Deprecations Added in Glimmer Internals
§ 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: constructor
s 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
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);
}
}