Skip to content

Instantly share code, notes, and snippets.

@mna
Created April 12, 2012 23:48
Show Gist options
  • Save mna/2371954 to your computer and use it in GitHub Desktop.
Save mna/2371954 to your computer and use it in GitHub Desktop.
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
Copy link
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