Ember.js - Taming computed properties

In Ember.js there is this feature called computed properties. You can learn more about what computed properties are in the official Ember.js guides. Instead I wanted to talk about how they act depending on what you pass to .property().

Story

I didn’t know much about Ember so I decided to give a talk about it at my local JavaScript meetup. It’s a great way to force yourself into doing something :).

Anyway, I was watching and taking notes from “Introduction to Ember.js” presentation by Luke Melia which he gave a couple of months ago at NYC Ember meetup. At some point Luke was explaining computed properties and what’s the difference between .property("content") and .property("content.@each.isDone") (assuming content is an array consisting of objects and these objects have isDone property which is set either to true or false).

This is where I decided to basically take what Luke said and turn it into a blog post.

Big picture

To better illustrate how these computed properties act depending on what gets passed to .property() I wrote yet another TODO application example. A task list with ability to add items and mark them as completed is a perfect example. Here’s a complete JSBin for you to play around with and see the end result. Don’t get carried away just yet because I still want to walk you through some of the code and explain what is what.

Ember Starter Kit

As you can see in the Output window we already have two todos. You can mark each of those todos as (in)complete by clicking a checkbox right next to them. You can add new todo items by clicking Add button. You can’t delete a todo, though but we can live without it in this example. There are also three counters each displaying a result depending on what I have passed to .property().

Give me the codes

I started by creating two objects with the same properties. The only difference is that isDone property for the secondTodo is set to false.

1
2
3
4
5
6
7
8
var firstTodo = Ember.Object.create({
  name: "First Todo",
  isDone: true
});
var secondTodo = Ember.Object.create({
  name: "Second Todo",
  isDone: false
});

I used these objects in the IndexRoute model hook to wire them up with index template.

1
2
3
4
5
App.IndexRoute = Ember.Route.extend({
  model: function() {
    return [firstTodo, secondTodo];
  }
});

Next, I wrote IndexController. It inherits from Ember.ArrayController because I’m representing a collection of models (firstTodo and secondTodo) and this way content property will be automatically set up to have these two objects. YAY for Ember’s Convention over Configuratin philosophy.

1
2
3
App.IndexController = Ember.ArrayController.extend({
  // content property of this controller holds [firstTodo, secondTodo]
});

I wanted to add additional todo objects on the fly so I wrote addTodo event handler in the IndexController. Nothing fancy. It creates and instance of Ember.Object and pushes it onto content array.

1
2
3
4
5
6
7
8
9
10
11
12
App.IndexController = Ember.ArrayController.extend({
  actions: {
    addTodo: function() {
      var newTodo = Ember.Object.create({
        name: "New Todo",
        isDone: false
      });

      return this.get("content").pushObject(newTodo);
    }
  }
});

Now we’re cooking

Now comes the interesting part – computed properties. Let’s start with the last one:

1
2
3
4
5
6
7
App.IndexController = Ember.ArrayController.extend({
  // ... skipped unrelavent code ...

  remainingThirdExample: function() {
    return this.get("content").filterBy("isDone", false).get("length");
  }.property("content")
});

On line 5 we grab the content array (remember it holds firstTodo and secondTodo objects). filterBy("isDone", false) will return an array which only has objects whos isDone property is set to false. Finally we get the length of that array which corresponds to the remaining item count.


Given .property("content") do you know what remainingThirdExample will return when we will add new object to the content array or change isDone property on an existing object? Anyone?

Say we have two todos in the content array and for one of them isDone property is set to false remainingThirdExample will return 1. Reason being we’re observing the array itself (content) and not the elements in it.

Go on

Moving on to the remainingSecondExample computed property:

1
2
3
4
5
6
7
8
9
App.IndexController = Ember.ArrayController.extend({
  // ... skipped unrelavent code ...

  remainingSecondExample: function() {
    return this.get("content").filterBy("isDone", false).get("length");
  }.property("content.@each")

  // ... skipped unrelavent code ...
});

.property("content.@each") basically means observe each element in the content array. Ok, great. This must work because now instead of observing the content array itself we’re looking for the changes in the array content. That’s at least what I was thinking. Turns out I was wrong. If you play with the Output window above you’ll notice that the count increments each time we add new todo but it doesn’t decrement when we (un)check some of the todos marking them (un)complete.

.property("content.@each") looks whether objects are being added or deleted from the content array. That’s why it properly increments count when we add new object with isDone property set to false but doesn’t decrement the count when we mark todo as not completed by unchecking the checkbox.

It works

Now, the version that works as expected:

1
2
3
4
5
6
7
8
9
App.IndexController = Ember.ArrayController.extend({
  // ... skipped unrelavent code ...

  remainingFirstExample: function() {
    return this.get("content").filterBy("isDone", false).get("length");
  }.property("content.@each.isDone")

  // ... skipped unrelavent code ...
});

By now you should see why .property("content.@each.isDone") works. It observes isDone property on each object that is in content array and recalculates the remaining todos each time isDone property gets changed in some object.

What’s missing

I didn’t touch the HTML part here because I think it’s pretty straight forward but please raise your voice in the comments in case something isn’t clear.

Also I did ask Luke and Stefan for their opinion on this article and they both pointed me to Ember.reduceComputed feature. At first I did want to change the existing code but then decided to write about in a separate blog post.

Final words

I really like Ember.js so I plan to write more about it in the near future.

I hope this was helpful. Till next post.


I'm currently looking for Ruby on Rails and/or Ember.js consulting gigs. Have anything in mind? Please don't hesitate to click here and get in touch with me. I don't bite ;)

Comments