Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
This gist shows one way to implement read- and write-enabled computed properties on a Backbone Model, without polluting the attributes hash of the Backbone Model. It works with Backbone.ModelBinding, so computed properties may be bound to form elements.
/*global _, Backbone*/
// By Martin Angers (PuerkitoBio)
// This should be inside a module pattern. Defines a base Model class, could/should be merged with your
// own base Model.
// Dependencies: underscore (_) and Backbone.
var ComputedProperties = function(model) {
this.model = model;
this.properties = {};
};
_.extend(ComputedProperties.prototype, {
add: function() {
var prop, compProp;
if (arguments.length === 1) {
// Assume a property object
prop = arguments[0];
} else {
// Assume separate parameters
prop = {};
prop.name = arguments[0];
prop.attributes = arguments[1];
prop.getter = arguments[2];
prop.setter = arguments[3];
}
if (!prop.name) {
throw new Error("Computed property must have a name.");
}
// Set the parent model on the computed property
_.extend(prop, {model: this.model});
// Add to the "properties" hash
this.properties[prop.name] = compProp = new ComputedProperty(prop);
// Start listening for change events on dependent attributes
compProp.bind();
return this;
},
remove: function(name) {
var compProp;
compProp = this.properties[name];
if (compProp) {
compProp.unbind();
delete this.properties[name];
}
return this;
},
clear: function() {
_.each(this.properties, function(prop) {
this.remove(prop.name);
}, this);
return this;
}
});
var ComputedProperty = function(prop) {
_.extend(this, prop);
};
_.extend(ComputedProperty.prototype, {
hasGetter: function() {
var getter = this.getter;
return (getter && typeof getter === 'function');
},
get: function() {
var val;
if (this.hasGetter()) {
val = this.getter.apply(this.model);
}
return val;
},
hasSetter: function() {
var setter = this.setter;
return (setter && typeof setter === 'function');
},
set: function(val, opts) {
var args;
if (this.hasSetter()) {
// Transform to real array, since some browsers will fail if an arguments pseudo-array is passed
args = Array.prototype.slice.call(arguments);
this.setter.apply(this.model, args);
// Let the underlying attributes trigger the change, which will in turn
// trigger the computed property's change event. If the setter doesn't actually
// set one of the dependent attributes, you should manually trigger the "change:compPropName"
// event.
}
},
isReadOnly: function() {
return (this.hasGetter() && !this.hasSetter());
},
isReadWrite: function() {
return (this.hasGetter() && this.hasSetter());
},
isWriteOnly: function() {
return (this.hasSetter() && !this.hasGetter());
},
onAttributeChange: function(model, val) {
var newVal;
if (model && model.trigger) {
newVal = this.get();
model.trigger("change:" + this.name, model, newVal);
}
},
bind: function() {
var attrs = this.attributes;
// Listen to all change events on dependent attributes
if (this.model && this.model.on && !_.isEmpty(attrs)) {
for (var i = 0; i < attrs.length; i++) {
this.model.on("change:" + attrs[i], this.onAttributeChange, this);
}
}
},
unbind: function() {
var attrs = this.attributes;
// Unbind from all change events on dependent attributes
if (this.model && this.model.off && !_.isEmpty(attrs)) {
for (var i = 0; i < attrs.length; i++) {
this.model.off("change:" + attrs[i], this.onAttributeChange, this);
}
}
}
});
Models.BaseModel = Backbone.Model.extend({
constructor: function() {
this.computedProperties = new ComputedProperties(this);
// Transform to real array, since some browsers will fail if an arguments pseudo-array is passed
var args = Array.prototype.slice.call(arguments);
Backbone.Model.prototype.constructor.apply(this, args);
},
get: function(attr) {
var props = this.computedProperties.properties, val,
args;
// Computed props take precedence over real attributes, should you name both the same...
if (_.has(props, attr)) {
val = props[attr].get();
} else {
// Transform to real array, since some browsers will fail if an arguments pseudo-array is passed
args = Array.prototype.slice.call(arguments);
val = Backbone.Model.prototype.get.apply(this, args);
}
return val;
},
set: function(key, val, opts) {
var attrs, keys, props = this.computedProperties.properties, args;
// Check if this is a attrs object or a single key-value pair
if (_.isObject(key) || _.isNull(key)) {
attrs = key;
opts = val;
} else {
attrs = {};
attrs[key] = val;
}
// Special case: backbone allows passing a Model to set, in which case all attributes
// are set. Since computed properties are NOT attributes, don't hook into this use-case.
if (!(attrs instanceof Backbone.Model)) {
keys = _.keys(attrs);
_.each(keys, function(key) {
// Check for computed properties, should opts.unset be passed, it is the responsibility
// of the setter of the computed property to act accordingly.
if (_.has(props, key)) {
props[key].set(attrs[key], opts);
delete attrs[key];
}
}, this);
}
// Call Backbone's standard "set" and return whatever it returns! *Important!*
return Backbone.Model.prototype.set.call(this, attrs, opts);
}
});
@mna

This comment has been minimized.

Copy link
Owner Author

mna commented Apr 13, 2012

Context and explanation in my blog post (in French): http://hypermegatop.calepin.co/proprietes-calculees-avec-backbone.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.