Software, life, and random thoughts
Home/Categories/Javascript/Backbone.js computed properties/

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.

©2023 Uzi Kilon