Dealing with non-persisted attributes in Backbone.js has been doing my head in for a while, particularly since I've started using ember/ember-data, which handles the various situations through computed properties, ember-data attributes, or controllers.
Many solutions suggest customising the toJSON
method. However, some popular Backbone plugins (particularly those that deal with nested models), implement their own toJSON
method, and make a call to Backbone.Model.prototype.toJSON
to obtain an object representation of a model's attributes. So by overwriting the toJSON
method in a model definition, you'll lose some (potentially crucial) features of those plugins.
The best I've come up with is to include an excludeFromJSON
array of keys in the model definition, and overwrite the toJSON
method on Backbone.Model.prototype
itself:
Backbone.Model.prototype.toJSON = function() {
var json = _.clone(this.attributes),
excludeFromJSON = this.excludeFromJSON;
if(excludeFromJSON) {
_.each(excludeFromJSON, function(key) {
delete json[key];
});
}
return json;
};
MyModel = Backbone.Model.extend({
excludeFromJSON: [
'inches'
]
});
In this way, you'll only have to define the non-persisted keys (if you forget to do so, you'll soon be reminded when your server throws an error!). toJSON
will behave as normal if no excludeFromJSON
property is present.
In your case, inches
is a computed property, derived from mm
, so it makes sense to implement this as a method on your model (ensuring that the value for inches is correct when mm is changed):
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
However, this has the downside of being accessed differently to everyother attribute. Ideally you'll want to keep it consistent with accessing other attributes. This can be achieved by extending the default get
method:
var getMixin = {
get: function(attr) {
if(typeof this[attr] == 'function') {
return this[attr]();
}
return Backbone.Model.prototype.get.call(this, attr);
}
};
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
_.extend(MyModel.prototype, getMixin);
Which will let you do:
new MyModel().get('inches');
This approach doesn't touch the underlying attributes
hash, meaning that inches
will not appear in the toJSON
representation, unless you set
the value of inches
later on, in which case you'll need something like the excludeFromJSON
array.
If you have the need to set
the inches
value, you may also want to listen for changes and adjust the value of mm
:
MyModel = Backbone.Model.extend({
initialize: function() {
this.on('change:inches', this.changeInches, this);
},
inches: function() {
return this.get('mm') / 25;
},
changeInches: function() {
this.set('mm', this.attributes.inches * 25);
}
});
_.extend(MyModel.prototype, getMixin);
See the complete example on JSBin.
It's also worth noting that the (official?) purpose of the toJSON
method has recently been redefined as preparing a model for syncing with a server. For this reason, calling toJSON
should always return only the "persistable" (or "saveable") attributes.