Ember recently converted to a modules based API where you import what you use rather than the whole Ember namespace. This has a number of benefits, including enabling tree-shaking (where we remove all unused code from the final build of the app) and splitting Ember into smaller, self-contained libraries.
You can read up on the JS Module API in and it's usage in the RFC.
For Ember Data, another RFC
is currently in the works to nail down the exact modules. In the meantime, we should
import the DS
namespace and destructure values off of it to emulate importing modules
directly.
// Good
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import DS from 'ember-data';
const {
Model,
attr
} = DS;
export default Model.extend({
firstName: attr('string'),
lastName: attr('string'),
surname: alias('lastName'),
fullName: computed('firstName', 'lastName', function() {
// Code
})
});
// Bad
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
surname: Ember.computed.alias('lastName'),
fullName: Ember.computed('firstName', 'lastName', function() {
// Code
})
});
Avoid Ember's Date
, Function
and String
prototype extensions. Prefer the
corresponding functions from the Ember
object.
Preferably turn the prototype extensions off by updating the
EmberENV.EXTEND_PROTOTYPES
setting in your config/environment
file.
module.exports = function(environment) {
var ENV = {
EmberENV: {
EXTEND_PROTOTYPES: {
Date: false,
Function: false,
String: false
}
}
// Good
export default Model.extend({
hobbies: w('surfing skateboarding skydiving'),
fullName: computed('firstName', 'lastName', function() { ... }),
didError: on('error', function() { ... })
});
// Bad
export default Model.extend({
hobbies: 'surfing skateboarding skydiving'.w(),
fullName: function() { ... }.property('firstName', 'lastName'),
didError: function() { ... }.on('error')
});
Array extensions should be avoided. Ember Arrays are necessary for watching array changes in computeds and in templates, but they should be used explicitly when possible, especially in addons:
// good
let foo = [];
foo.pushObject('bar');
// better
import { A } from '@ember/array';
let foo = A();
foo.pushObject('bar');
Ordering a module's properties in a predictable manner will make it easier to scan.
-
Plain properties
Start with properties that configure the module's behavior. Examples are
tagName
andclassNames
on components andqueryParams
on controllers and routes. Followed by any other simple properties, like default values for properties. -
Single line computed property macros
E.g.
alias
,sort
and other macros. Start with service injections. If the module is a model, thenattr
properties should be first, followed bybelongsTo
andhasMany
. -
Multi line computed property functions
-
Lifecycle hooks
The hooks should be chronologically ordered by the order they are invoked in.
-
Functions
Public functions first, internal functions after.
-
Actions
export default Component.extend({
// Plain properties
tagName: 'span',
// Single line CP
post: alias('myPost'),
// Multiline CP
authorName: computed('author.{firstName,lastName}', function() {
// code
}),
// Lifecycle hooks
didReceiveAttrs() {
this._super(...arguments);
// code
},
// Functions
someFunction() {
// code
},
actions: {
someAction() {
// Code
}
}
});
Rather than using the object's init
hook via on
, override init and
call _super
with ...arguments
. This allows you to control execution
order. Don't Don't Override Init
Models should be grouped as follows:
- Attributes
- Associations
- Computed Properties
Within each section, the attributes should be ordered alphabetically.
// Good
import { computed } from '@ember/object';
import DS from 'ember-data';
const {
Model,
attr,
hasMany
} = DS;
export default Model.extend({
// Attributes
firstName: attr('string'),
lastName: attr('string'),
// Associations
children: hasMany('child'),
// Computed Properties
fullName: computed('firstName', 'lastName', function() {
// Code
})
});
// Bad
import { computed } from '@ember/object';
import DS from 'ember-data';
const {
Model,
attr,
hasMany
} = DS;
export default Model.extend({
children: hasMany('child'),
firstName: attr('string'),
lastName: attr('string'),
fullName: computed('firstName', 'lastName', function() {
// Code
})
});
For consistency and ease of discovery, list your query params first in your controller. These should be listed above default values.
It provides a cleaner code to name your model user
if it is a user. It
is more maintainable
export default Controller.extend({
user: alias('model')
});
Always use components. Partials share scope with the parent view, using components will provide a consistent scope.
For a demonstration of why this is bad, consider if the following Javascript was valid:
function logX() {
console.log(x);
}
function foo() {
let x = 4;
function bar() {
let x = 'baz';
logX();
}
logX(); // '4'
bar(); // 'baz'
}
This is essentially what partials are doing by pulling variables, bindings, and values from whichever scope they are used in. It creates brittle, error prone code without a strong contract over its usage.
Note: Pending Ember 1.13, the hash helper is not yet available
Use the hash helper to yield what you need instead.
Note: Pending Ember 1.13, the action keyword is not yet available
Although it's not strictly needed to use the action
keyword to pass on
actions that have already been passed with the action
keyword once,
it's recommended to always use the action
keyword when passing an action
to another component. This will prevent some potential bugs that can happen
and also make it more clear that you are passing an action.
Ultimately, we should make it easier for other developers to read templates. Ordering attributes and then action helpers will provide clarity.
The model hooks are async hooks, and will wait for any promises returned to resolve. An example of this would be models needed to fill a drop down in a form, you don't want to render this page without the options in the dropdown. A counter example would be comments on a page. The comments should be fetched along side the model, but should not block your page from loading if the required model is there. For more detailed information on when to load data in routes and when to load it somewhere else, see this breakdown from Edward Faulkner.
Now that ES Classes have solidified and features like decorators and properties have begun to be built out, Ember is beginning to move toward using them, and long run they will become the standard. We can begin using ES Classes now, with certain caveats.
NOTE: ES Classes are not truly usable without decorators which are currently stage 2 in TC39 and class fields which are currently stage 3. Until both proposals are at least stage 3 we can't recommend their usage as truly stable, so they should only be used in addons and relatively small applications.
Before:
import Ember from 'ember';
export default Ember.Component.extend({
foo: Ember.inject.service(),
bar: Ember.computed('someKey', 'otherKey', function() {
var someKey = this.get('someKey');
var otherKey = this.get('otherKey');
return `${someKey} - ${otherKey}`;
}),
actions: {
handleClick() {
// do stuff
}
}
});
After:
import Component from '@ember/component'
import { action, computed } from 'ember-decorators/object';
import { service } from 'ember-decorators/service';
export default class ExampleComponent extends Component {
@service foo
@computed('someKey', 'otherKey')
bar(someKey, otherKey) {
return `${someKey} - ${otherKey}`;
}
@action
handleClick() {
// do stuff
}
}
- Use the ember-decorators library
- Always specify a class name rather than using anonymous classes. This gives the prototype a name, which will allow us to identify instances, and also makes it easier to grep the codebase for classes.
- Use the
constructor
rather thaninit
- Assign default values in the
constructor
or using class fields
// before
export default Ember.Component.extend({
foo: 'bar'
});
// after
export default class ExampleComponent extends Component {
constructor() {
this.super(...arguments);
this.foo = this.foo !== undefined ? this.foo : 'bar';
}
}