Backbone.js computed properties

A simple yet powerful way to create backbone model calculated fields or “macros”

For one of our applications, we needed a way to store “macros” in backbone models, that will return a calculated value, based on values we have in the model fields.

I came up with a quick solution that seems to be good enough so far.
the concept is, the same way you can set anything to a field value, a function in javascript IS a value, so nothing holds you from setting a function in a field:

var myModel = new Backbone.Model();
myModel.set('func', function(){
    return 'hello calculated field!';
}, {silent: true}); // bypass the change event

Later on we can get the value of this function with a call such as:

var value = myModel.get('func').call(myModel);

This works, but it is not very convenient.
I wanted an automated way to use calculated methods without any side effects, which will include the field results in the model signature.
JSON.stringify ignores functions so consider this:

    
var foo = {
    bar: function(){
        return 'bazz';
    }
};
JSON.stringify(foo); // prints "{}"

Based on that, the original backbone toJSON will just work as is, ignoring our macros.
However, if we wanted to have the values our macros returns in our signature, we want to make sure it is being called, returning the resut of the value rather than the function.

I came out with this:

var BaseModel = Backbone.Model.extend({
  get: function(attr) {
    var value = Backbone.Model.prototype.get.call(this, attr);
    return _.isFunction(value) ? value.call(this) : value;
  },
  toJSON: function() {
    var data = {};
    var json = Backbone.Model.prototype.toJSON.call(this);
    _.each(json, function(value, key) {
      data[key] = this.get(key);
    }, this);
    return data;
  }
});

As the code tells us, in our new get method we are returning values of calculated fields, and in our new toJSON method we are including computed properties by values.

This could be great, but we have to remember that model.toJSON() is what Backbone sends back to the server when we perform a save operation, so it may be a good idea, depending on the server implementation, to exclude all computed properties from the model signature:

toJSON: function() {
  var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
  _.each(json, function (value, key) {
    if (typeof value == 'function') {
      delete json[key];
    }
  });
  return json;
}

An implementation could look like:

var MyModel = BaseModel.extend({
    defaults: {
        avg: function(){
            return (this.get('min') + this.get('max')) / 2;
        }
    }
});
var myModel = new MyModel({min: 1, max: 17});
var avg = myModel.get('avg'); // 9

One “drawback” we need to be aware of, is due to the nature of the implementation, you can not bind to change events on this field, since the value stays the function and what changes is the computed value it returns.

Simple? Powerful?

EDIT 3/14/2012:
If you do want to observe changes when a computed property value changes, you will need the system to be aware of which attributes are affecting it state.
This could be done like that:

var avg = function(){
    return (this.get('min') + this.get('max')) / 2;
};
avg.attributes = ['min', 'max'];
myModel.set('avg', avg);

Now we have a way to track which fields are impacting our computed property.
What’s left to do is observe changes on these fields, and fire a change event on our property when these happen.
We could attach the event listener on the “set” method, but that would mean we will need to keep track on when we unset, reset, etc and remove that listener.

A cleaner (yet a little less efficient) approach would be to listen to all change events, and figure out upon it if a related field had changed.
This could be done like this:

myModel.on('change', function(){
  var i, changedAttributes = this.changedAttributes() || [];
  _.each(this.attributes, function(value, key){
    if( _.isFunction(value) && _.isArray(value.attributes) ) {
      for(i in value.attributes) {
        if ( _.has(changedAttributes, value.attributes[i]) ) {
          this.trigger("change:"+key);
          return ;
        }
      }
    }
  }, this);
}, myModel);

We can add this to out initialize method, and get it automated, so our complete model will look like:

var BaseModel = Backbone.Model.extend({
  initialize: function(){
    this.on('change', function(){
      var i, changedAttributes = this.changedAttributes() || [];
      _.each(this.attributes, function(value, key){
        if( _.isFunction(value) && _.isArray(value.attributes) ) {
          for(i in value.attributes) {
            if ( _.has(changedAttributes, value.attributes[i]) ) {
              this.trigger("change:"+key);
              return ;
            }
          }
        }
      }, this);
    }, this);
  },
  get: function(attr) {
    var value = Backbone.Model.prototype.get.call(this, attr);
    return _.isFunction(value) ? value.call(this) : value;
  },
  toJSON: function() {
    var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
    _.each(json, function (value, key) {
      if (typeof value == 'function') {
        delete json[key];
      }
    });
    return json;
  }
});

The question is: since a computed property is a macro/helper and not a “real” part of our data model, and they never do actually change, should we allow then to trigger change event?
I personally believe we shouldn’t use this approach, for it abstracts the true nature of the component. However, it can be useful.

Lead UI Engineer and a Software Architect.
Over 12 years of professional experience, writing complex web applications, and still learning something new every day.
Currently working for Okta in San Francisco

Twitter LinkedIn Google+ 

12 thoughts on “Backbone.js computed properties

  1. Derick Bailey

    nice approach! I’ve been trying different ways of handling this for a long time now, and haven’t seen anything quite like this. will definitely have to try it and see what i think :)

    Reply
  2. Bernardo

    Good to see other developers reaching similar solutions as I have… However, like you mentioned, I’m stuck on binding to ‘change’ events on the computed property. I don’t want to have to manually do it, but it seems like we’d require a ton of custom coding to allow firing of a ‘change’ event when a computed property changes.

    Reply
    1. Uzi Kilon Post author

      I just edited my post with one approach for how to do observe changes on computed properties.
      Personally I don’t think it is the “right” thing to do, but is fairly straightforward to add support for it.

      Reply
  3. Tim

    Instead of all the listening, wouldn’t it make sense to do something similar to the Ember method in the computable fields of your final model, and override the set method on your base model as well?


    var ComputableModel = Backbone.Model.extend({
    get: function(attr) {
    var value = Backbone.Model.prototype.get.call(this, attr);
    if (typeof value === "function") {
    return value.call(this);
    } else {
    return value;
    }
    },
    set: function(key, value, options) {
    // simple override -- not offering support for setting multiples with {key:value,...}
    var property = _.isObject(key) ? '' : Backbone.Model.prototype.get.call(this, key);
    if (typeof property === "function") {
    return property.call(value);
    } else {
    Backbone.Model.prototype.set.call(key, value, options);
    }
    },
    toJSON: function() {
    var data = {},
    json = Backbone.Model.prototype.toJSON.call(this);
    _.each(json, function(value, key) {
    if(typeof value !== 'function') {
    data[key] = value;
    }
    });
    return data;
    }
    });

    var Contact = ComputableModel.extend({
    firstName: '',
    lastName: '',
    fullName: function(value) {
    if (arguments.length === 0) { // getter
    return this.get('firstName') + ' ' + this.get('lastName');
    } else { // setter
    var name = value.split(" ");

    this.set('firstName', name[0]);
    this.set('lastName', name[1]);

    return value;
    }
    }
    });

    Reply
    1. Uzi Kilon Post author

      That is another approach that tries to solve a more complex problem of 2 way binding between properties and computed properties.
      However, the example above is a naive implementation that solves part of the problem.
      Also, it doesn’t solve the event binding issue either since we are dealing with separate properties (‘change:fullName’ does not fire when ‘firstName’ changes)

      Reply
  4. mhume

    Hi Uzi
    Sorry i’m late to the party but i just had to add to this great post!
    In one of your first examples you stipulate it maybe advisable to remove the methods (functions) from the attributes before saving the model.
    When toJSON is invoked in Backbone.sync the attributes are passed into JSON.stringify which automatically removes methods.
    This coupled with save detection within toJSON gives the following code. (save uses basic properties, other calls to toJSON include the calculated properties)

    var BaseModel = Backbone.Model.extend({
    get: function(attr) {
    var value = Backbone.Model.prototype.get.call(this, attr);
    return _.isFunction(value) ? value.call(this) : value;
    },
    toJSON: function(options) {
    var json = Backbone.Model.prototype.toJSON.call(this),
    data = {};
    if(_.has(options||{}, ‘emulateHTTP’)){ // its a save
    return json; // JSON.stringify will remove the methods for us
    }
    _.each(json, function(value, key) {
    data[key] = this.get(key);
    }, this);
    return data;
    }
    });

    hope this helps someone
    mh

    Reply
  5. AJ

    Normally you would just listen to the attributes that change.

    ` initialize: function() {
    ` this.on(‘change:min change:max’, this.setAvg, this);
    ` },
    ` setAvg: function() {
    ` this.set(‘avg’, (this.get(‘min’) + this.get(‘max’))/2);
    ` }

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>