Created
April 12, 2012 23:48
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*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); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Context and explanation in my blog post (in French): http://hypermegatop.calepin.co/proprietes-calculees-avec-backbone.html