The Object Model
Classes and Instances
To define a new Ember class, call the extend() method on
Ember.Object:
1 App.Person = Ember.Object.extend({
2 say: function(thing) {
3 alert(thing);
4 }
5 });
This defines a new App.Person class with a say() method.
You can also create a subclass from any existing class by calling
its extend() method. For example, you might want to create a subclass
of Ember’s built-in Ember.View class:
1 App.PersonView = Ember.View.extend({
2 tagName: 'li',
3 classNameBindings: ['isAdministrator']
4 });
When defining a subclass, you can override methods but still access the
implementation of your parent class by calling the special _super()
method:
1 App.Person = Ember.Object.extend({
2 say: function(thing) {
3 var name = this.get('name');
4 alert(name + " says: " + thing);
5 }
6 });
7
8 App.Soldier = App.Person.extend({
9 say: function(thing) {
10 this._super(thing + ", sir!");
11 }
12 });
13
14 var yehuda = App.Soldier.create({
15 name: "Yehuda Katz"
16 });
17
18 yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!"
Creating Instances
Once you have defined a class, you can create new instances of that
class by calling its create() method. Any methods, properties and
computed properties you defined on the class will be available to
instances:
1 var person = App.Person.create();
2 person.say("Hello"); // alerts " says: Hello"
When creating an instance, you can initialize the value of its properties
by passing an optional hash to the create() method:
1 App.Person = Ember.Object.extend({
2 helloWorld: function() {
3 alert("Hi, my name is " + this.get('name'));
4 }
5 });
6
7 var tom = App.Person.create({
8 name: "Tom Dale"
9 });
10
11 tom.helloWorld(); // alerts "Hi, my name is Tom Dale"
For performance reasons, note that you cannot redefine an instance’s
computed properties or methods when calling create(), nor can you
define new ones. You should only set simple properties when calling
create(). If you need to define or redefine methods or computed
properties, create a new subclass and instantiate that.
By convention, properties or variables that hold classes are
PascalCased, while instances are not. So, for example, the variable
App.Person would point to a class, while person would point to an instance
(usually of the App.Person class). You should stick to these naming
conventions in your Ember applications.
Initializing Instances
When a new instance is created, its init method is invoked
automatically. This is the ideal place to do setup required on new
instances:
1 App.Person = Ember.Object.extend({
2 init: function() {
3 var name = this.get('name');
4 alert(name + ", reporting for duty!");
5 }
6 });
7
8 App.Person.create({
9 name: "Stefan Penner"
10 });
11
12 // alerts "Stefan Penner, reporting for duty!"
If you are subclassing a framework class, like Ember.View or
Ember.ArrayController, and you override the init method, make sure
you call this._super()! If you don’t, the system may not have an
opportunity to do important setup work, and you’ll see strange behavior
in your application.
When accessing the properties of an object, use the get
and set accessor methods:
1 var person = App.Person.create();
2
3 var name = person.get('name');
4 person.set('name', "Tobias Fünke");
Make sure to use these accessor methods; otherwise, computed properties won’t recalculate, observers won’t fire, and templates won’t update.
Computed Properties
What are Computed Properties?
In a nutshell, computed properties let you declare functions as properties. You create one by defining a computed property as a function, which Ember will automatically call when you ask for the property. You can then use it the same way you would any normal, static property.
It’s super handy for taking one or more normal properties and transforming or manipulating their data to create a new value.
Computed properties in action
We’ll start with a simple example:
1 App.Person = Ember.Object.extend({
2 // these will be supplied by `create`
3 firstName: null,
4 lastName: null,
5
6 fullName: function() {
7 return this.get('firstName') + ' ' + this.get('lastName');
8 }.property('firstName', 'lastName')
9 });
10
11 var ironMan = App.Person.create({
12 firstName: "Tony",
13 lastName: "Stark"
14 });
15
16 ironMan.get('fullName'); // "Tony Stark"
Notice that the fullName function calls property. This declares the function to be a computed property, and the arguments tell Ember that it depends on the firstName and lastName attributes.
Whenever you access the fullName property, this function gets called, and it returns the value of the function, which simply calls firstName + lastName.
Alternate invocation
At this point, you might be wondering how you are able to call the .property function on a function. This is possible because Ember extends the function prototype. More information about extending native prototypes is available in the disabling prototype extensions guide. If you’d like to replicate the declaration from above without using these extensions you could do so with the following:
1 fullName: Ember.computed('firstName', 'lastName', function() {
2 return this.get('firstName') + ' ' + this.get('lastName');
3 })
Chaining computed properties
You can use computed properties as values to create new computed properties. Let’s add a description computed property to the previous example, and use the existing fullName property and add in some other properties:
1 App.Person = Ember.Object.extend({
2 firstName: null,
3 lastName: null,
4 age: null,
5 country: null,
6
7 fullName: function() {
8 return this.get('firstName') + ' ' + this.get('lastName');
9 }.property('firstName', 'lastName'),
10
11 description: function() {
12 return this.get('fullName') + '; Age: ' + this.get('age') + '; Country: ' + \
13 this.get('country');
14 }.property('fullName', 'age', 'country')
15 });
16
17 var captainAmerica = App.Person.create({
18 firstName: 'Steve',
19 lastName: 'Rogers',
20 age: 80,
21 country: 'USA'
22 });
23
24 captainAmerica.get('description'); // "Steve Rogers; Age: 80; Country: USA"
Dynamic updating
Computed properties, by default, observe any changes made to the properties they depend on and are dynamically updated when they’re called. Let’s use computed properties to dynamically update.
1 captainAmerica.set('firstName', 'William');
2
3 captainAmerica.get('description'); // "William Rogers; Age: 80; Country: USA"
So this change to firstName was observed by fullName computed property, which was itself observed by the description property.
Setting any dependent property will propagate changes through any computed properties that depend on them, all the way down the chain of computed properties you’ve created.
Setting Computed Properties
You can also define what Ember should do when setting a computed property. If you try to set a computed property, it will be invoked with the key (property name), the value you want to set it to, and the previous value.
1 App.Person = Ember.Object.extend({
2 firstName: null,
3 lastName: null,
4
5 fullName: function(key, value, previousValue) {
6 // setter
7 if (arguments.length > 1) {
8 var nameParts = value.split(/\s+/);
9 this.set('firstName', nameParts[0]);
10 this.set('lastName', nameParts[1]);
11 }
12
13 // getter
14 return this.get('firstName') + ' ' + this.get('lastName');
15 }.property('firstName', 'lastName')
16 });
17
18
19 var captainAmerica = App.Person.create();
20 captainAmerica.set('fullName', "William Burnside");
21 captainAmerica.get('firstName'); // William
22 captainAmerica.get('lastName'); // Burnside
Ember will call the computed property for both setters and getters, so if you want to use a computed property as a setter, you’ll need to check the number of arguments to determine whether it is being called as a getter or a setter. Note that if a value is returned from the setter, it will be cached as the property’s value.
Computed Properties and Aggregate Data with @each
Often, you may have a computed property that relies on all of the items in an array to determine its value. For example, you may want to count all of the todo items in a controller to determine how many of them are completed.
Here’s what that computed property might look like:
1 App.TodosController = Ember.Controller.extend({
2 todos: [
3 Ember.Object.create({ isDone: true }),
4 Ember.Object.create({ isDone: false }),
5 Ember.Object.create({ isDone: true })
6 ],
7
8 remaining: function() {
9 var todos = this.get('todos');
10 return todos.filterBy('isDone', false).get('length');
11 }.property('todos.@each.isDone')
12 });
Note here that the dependent key (todos.@each.isDone) contains the special
key @each. This instructs Ember.js to update bindings and fire observers for
this computed property when one of the following four events occurs:
- The
isDoneproperty of any of the objects in thetodosarray changes. - An item is added to the
todosarray. - An item is removed from the
todosarray. - The
todosproperty of the controller is changed to a different array.
In the example above, the remaining count is 1:
1 App.todosController = App.TodosController.create();
2 App.todosController.get('remaining');
3 // 1
If we change the todo’s isDone property, the remaining property is updated
automatically:
1 var todos = App.todosController.get('todos');
2 var todo = todos.objectAt(1);
3 todo.set('isDone', true);
4
5 App.todosController.get('remaining');
6 // 0
7
8 todo = Ember.Object.create({ isDone: false });
9 todos.pushObject(todo);
10
11 App.todosController.get('remaining');
12 // 1
Note that @each only works one level deep. You cannot use nested forms like
todos.@each.owner.name or todos.@each.owner.@each.name.
Observers
Ember supports observing any property, including computed properties.
You can set up an observer on an object by using the observes
method on a function:
1 Person = Ember.Object.extend({
2 // these will be supplied by `create`
3 firstName: null,
4 lastName: null,
5
6 fullName: function() {
7 var firstName = this.get('firstName');
8 var lastName = this.get('lastName');
9
10 return firstName + ' ' + lastName;
11 }.property('firstName', 'lastName'),
12
13 fullNameChanged: function() {
14 // deal with the change
15 }.observes('fullName').on('init')
16 });
17
18 var person = Person.create({
19 firstName: 'Yehuda',
20 lastName: 'Katz'
21 });
22
23 person.set('firstName', 'Brohuda'); // observer will fire
Because the fullName computed property depends on firstName,
updating firstName will fire observers on fullName as well.
Observers and asynchrony
Observers in Ember are currently synchronous. This means that they will fire as soon as one of the properties they observe changes. Because of this, it is easy to introduce bugs where properties are not yet synchronized:
1 Person.reopen({
2 lastNameChanged: function() {
3 // The observer depends on lastName and so does fullName. Because observers
4 // are synchronous, when this function is called the value of fullName is
5 // not updated yet so this will log the old value of fullName
6 console.log(this.get('fullName'));
7 }.observes('lastName')
8 });
This synchronous behaviour can also lead to observers being fired multiple times when observing multiple properties:
1 Person.reopen({
2 partOfNameChanged: function() {
3 // Because both firstName and lastName were set, this observer will fire twi\
4 ce.
5 }.observes('firstName', 'lastName')
6 });
7
8 person.set('firstName', 'John');
9 person.set('lastName', 'Smith');
To get around these problems, you should make use of Ember.run.once. This will
ensure that any processing you need to do only happens once, and happens in the
next run loop once all bindings are synchronized:
1 Person.reopen({
2 partOfNameChanged: function() {
3 Ember.run.once(this, 'processFullName');
4 }.observes('firstName', 'lastName'),
5
6 processFullName: function() {
7 // This will only fire once if you set two properties at the same time, and
8 // will also happen in the next run loop once all properties are synchronized
9 console.log(this.get('fullName'));
10 }
11 });
12
13 person.set('firstName', 'John');
14 person.set('lastName', 'Smith');
Observers and object initialization
Observers never fire until after the initialization of an object is complete.
If you need an observer to fire as part of the initialization process, you
cannot rely on the side effect of set. Instead, specify that the observer
should also run after init by using .on('init'):
1 App.Person = Ember.Object.extend({
2 init: function() {
3 this.set('salutation', "Mr/Ms");
4 },
5
6 salutationDidChange: function() {
7 // some side effect of salutation changing
8 }.observes('salutation').on('init')
9 });
Unconsumed Computed Properties Do Not Trigger Observers
If you never get a computed property, its observers will not fire even if
its dependent keys change. You can think of the value changing from one unknown
value to another.
This doesn’t usually affect application code because computed properties are almost always observed at the same time as they are fetched. For example, you get the value of a computed property, put it in DOM (or draw it with D3), and then observe it so you can update the DOM once the property changes.
If you need to observe a computed property but aren’t currently retrieving it, just get it in your init method.
Without prototype extensions
You can define inline observers by using the Ember.observer method if you
are using Ember without prototype extensions:
1 Person.reopen({
2 fullNameChanged: Ember.observer('fullName', function() {
3 // deal with the change
4 })
5 });
Outside of class definitions
You can also add observers to an object outside of a class definition using addObserver:
1 person.addObserver('fullName', function() {
2 // deal with the change
3 });
Bindings
A binding creates a link between two properties such that when one changes, the other one is updated to the new value automatically. Bindings can connect properties on the same object, or across two different objects. Unlike most other frameworks that include some sort of binding implementation, bindings in Ember.js can be used with any object, not just between views and models.
The easiest way to create a two-way binding is to use a computed alias, that specifies the path to another object.
1 wife = Ember.Object.create({
2 householdIncome: 80000
3 });
4
5 husband = Ember.Object.create({
6 wife: wife,
7 householdIncome: Ember.computed.alias('wife.householdIncome')
8 });
9
10 husband.get('householdIncome'); // 80000
11
12 // Someone gets raise.
13 husband.set('householdIncome', 90000);
14 wife.get('householdIncome'); // 90000
Note that bindings don’t update immediately. Ember waits until all of your application code has finished running before synchronizing changes, so you can change a bound property as many times as you’d like without worrying about the overhead of syncing bindings when values are transient.
One-Way Bindings
A one-way binding only propagates changes in one direction. Often, one-way bindings are just a performance optimization and you can safely use a two-way binding (as, of course, two-way bindings are de facto one-way bindings if you only ever change one side). Sometimes one-way bindings are useful to achieve specific behaviour such as a default that is the same as another property but can be overriden (e.g. a shipping address that starts the same as a billing address but can later be changed)
1 user = Ember.Object.create({
2 fullName: "Kara Gates"
3 });
4
5 userView = Ember.View.create({
6 user: user,
7 userName: Ember.computed.oneWay('user.fullName')
8 });
9
10 // Changing the name of the user object changes
11 // the value on the view.
12 user.set('fullName', "Krang Gates");
13 // userView.userName will become "Krang Gates"
14
15 // ...but changes to the view don't make it back to
16 // the object.
17 userView.set('userName', "Truckasaurus Gates");
18 user.get('fullName'); // "Krang Gates"
Reopening Classes and Instances
You don’t need to define a class all at once. You can reopen a class and
define new properties using the reopen method.
1 Person.reopen({
2 isPerson: true
3 });
4
5 Person.create().get('isPerson') // true
When using reopen, you can also override existing methods and
call this._super.
1 Person.reopen({
2 // override `say` to add an ! at the end
3 say: function(thing) {
4 this._super(thing + "!");
5 }
6 });
reopen is used to add instance methods and properties that are shared across all instances of a class. It does not add
methods and properties to a particular instance of a class as in vanilla JavaScript (without using prototype).
But when you need to create class methods or add properties to the class itself you can use reopenClass.
1 Person.reopenClass({
2 createMan: function() {
3 return Person.create({isMan: true})
4 }
5 });
6
7 Person.createMan().get('isMan') // true
Bindings, Observers, Computed Properties: What Do I Use When?
Sometimes new users are confused about when to use computed properties, bindings and observers. Here are some guidelines to help:
- Use computed properties to build a new property by synthesizing other properties. Computed properties should not contain application behavior, and should generally not cause any side-effects when called. Except in rare cases, multiple calls to the same computed property should always return the same value (unless the properties it depends on have changed, of course.)
- Observers should contain behavior that reacts to changes in another property. Observers are especially useful when you need to perform some behavior after a binding has finished synchronizing.
- Bindings are most often used to ensure objects in two different layers are always in sync. For example, you bind your views to your controller using Handlebars.