Observing Enumerables & Arrays with Ember.js

Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.

If you’ve built an app in Ember.js, by now you will be familiar with observers. Observers watch a given path (or paths), and run a function when the value at that path changes:

var Cow = Ember.Object.extend({
  weatherObserver: function(){
    if (this.get('willRain'){
      this.layDown();
    } else {
      this.standUp();
    }
  }.observes('willRain')
});

Enumerables (a group of items without order) and arrays can be observed in the same manner:

var Herd = Ember.Object.extend({
  migrateToPasture: function(){
    if (this.get('cows.length') > 30) {
      this.migrateToLargePasture();
    } else {
      this.migrateToSmallPasture();
    }
  }.observes('cows.@each')
});

However, Ember also provides a set of observers that share details about which items in an enumerable or array changed. Say hello to addEnumerableObserver and addArrayObserver. The API documentation for these functions isn’t that great, but they are fantastic and powerful tools once you learn how they work.

addEnumberableObserver

Enumberable observers [docs here] give you a simple way to see objects added into a list or being removed. The callback has two parts:

  1. Before an enumerable is changed, a “will change” callback is run.
  2. After an enumerable is changed, a “did change” callback is run.

The events receive different arguments:

  • “will change” callbacks receive the enumerable, the objects being removed, and the number of objects that will be added.
  • “did change” callback receive the enumerable, the number of objects removed, and the objects being added.

The argument order is a bit confusing, but the API is simple in practice.

var Cow = Ember.Object.extend();
var cows = [];
cows.addEnumerableObserver(this, {
  willChange: function(cows, removing, addCount){
    console.log('willChange', cows.length, removing.length, addCount);
  },
  didChange:function(cows, removeCount, adding){
    console.log('didChange', cows.length, removeCount, adding.length);
  }
});
cows.pushObject(new Cow());
// willChange 0 0 1
// didChange 1 0 1
var brownCow = new Cow();
cows.pushObjects([brownCow, new Cow()]);
// willChange 1 0 2
// didChange 3 0 2
cows.removeObject(brownCow);
// willChange 3 1 0
// didChange 2 1 0
cows.replace(0, 1, [new Cow()]);
// willChange 2 1 1
// didChange 2 1 1

Because enumerables do not care about the order of their items, these callbacks don’t inform you about where the objects were added or removed. Let’s use this observer to set the herd property on a given member cow.

var Cow = Ember.Object.extend();
var Herd = Ember.Object.extend({
  init: function(name){
    this._super();
    var cows = [];
    cows.addEnumerableObserver(this, {
      willChange: this.willChangeCows,
      didChange: this.didChangeCows
    });
    this.setProperties({
      cows: cows,
      name: name
    });
  },
  willChangeCows: function(cows, removing, addCount){
    removing.forEach(function(cow){ cow.set('herd', null); });
  },
  didChangeCows: function(cows, removeCount, adding){
    adding.forEach(function(cow){
      // Only allow the cow to be in a single herd
      if (cow.get('herd')) { cow.get('herd.cows').removeObject(cow) }
      cow.set('herd', this);
    }, this);
  }
});

var southHerd = new Herd('south');
var northHerd = new Herd('north');
var brownCow = new Cow();

southHerd.get('cows').pushObject(brownCow);

console.log( brownCow.get('herd.name') );    // south
console.log( southHerd.get('cows.length') ); // 1

northHerd.get('cows').pushObject(brownCow);

console.log( brownCow.get('herd.name') );    // north
console.log( southHerd.get('cows.length') ); // 0

Creating relationships between objects is one way to use these observers- another would be to synchronize a list without shipping the entire list for each update (say, over Pusher). With addEnumerableObserver, you can easily send only the data that changed.

Don’t forget to clean up any observers you may abandon with removeEnumerableObserver [docs here].

addArrayObserver

Array observers [docs here] in Ember are similar to enumerable observers, but include additional information about where in the array the change is taking place. Array observers power enumerable observers behind the scenes.

Here the arguments and API are a bit more consistent. Both callbacks receive the array, an offset, a removed item count, and an added item count.

var Cow = Ember.Object.extend();
var cows = [];
cows.addArrayObserver(this, {
  willChange: function(cows, offset, removeCount, addCount){
    console.log('willChange', cows.length, offset, removeCount, addCount);
  },
  didChange:function(cows, offset, removeCount, addCount){
    console.log('didChange', cows.length, offset, removeCount, addCount);
  }
});
cows.pushObject(new Cow());
// willChange 0 0 0 1
// didChange 1 0 0 1
var brownCow = new Cow();
cows.pushObjects([brownCow, new Cow()]);
// willChange 1 1 0 2
// didChange 3 1 0 2
cows.removeObject(brownCow);
// willChange 3 1 1 0
// didChange 2 1 1 0
herd.replace(1, 1, [new Cow()]);
// willChange 2 1 1 1
// didChange 2 1 1 1

Using the offset, you can fetch objects that will change from the array:

var cows = [];
cows.addArrayObserver(this, {
  willChange: function(cows, offset, removeCount, addCount){
    for (var i=offset; i<offset+removeCount; i++){
      console.log('will remove item', cows[i]);
    }
  },
  //...

Access objects that will be removed in willChange, because they will be removed by the time didChange is called. Likewise, you must access objects added to the array in didChange, since they won’t be added when willChange is called.

I’ve been using a syntax for both addEnumerableObserver and addArrayObserver that accepts the callbacks as arguments. Alternatively, you can use a syntax you will find sprinkled through the Ember.js codebase:

var cows = [];
var LoggingArray = Ember.Object({
  init: function(array){
    this._super();
    array.addArrayObserver(this);
  },
  arrayWillChange: function(cows, offset, removeCount, addCount){
    // ...
  },
  arrayDidChange: function(cows, offset, removeCount, addCount){
    // ...
  }
});
new LoggingArray(cows);

This syntax is nice for stand-along observers, but I tend to prefer the explicit arguments used in the other examples.

Ember gives developers some fantastic utilities outside of the framework itself. I hope you find these ones helpful!