backbone-associations
Advanced tools
Comparing version 0.4.2 to 0.5.0
@@ -1,7 +0,11 @@ | ||
(function(){var f,h,q,l,p,o,s;"undefined"!==typeof require?(f=require("underscore"),h=require("backbone"),exports=module.exports=h):(f=this._,h=this.Backbone);q=h.Model;l=h.Collection;p=q.prototype;s=/[\.\[\]]+/g;h.Many="Many";h.One="One";o=h.AssociatedModel=q.extend({relations:void 0,_proxyCalls:void 0,get:function(b){var a=p.get.call(this,b);return a?a:this.getAttr.apply(this,arguments)},set:function(b,a,d){var c,k,e,i,g=this;if(f.isObject(b)||b==null){c=b;d=a}else{c={};c[b]=a}if(!c)return this; | ||
for(k in c){e||(e={});if(k.match(s)){b=r(k);a=f.initial(b);b=b[b.length-1];a=this.get(a);if(a instanceof o){a=e[a.cid]||(e[a.cid]={model:a,data:{}});a.data[b]=c[k]}}else{a=e[this.cid]||(e[this.cid]={model:this,data:{}});a.data[k]=c[k]}}if(e)for(i in e){a=e[i];this.setAttr.call(a.model,a.data,d)||(g=false)}else return this.setAttr.call(this,c,d);return g},setAttr:function(b,a){var d;a||(a={});if(a.unset)for(d in b)b[d]=void 0;this.relations&&f.each(this.relations,function(c){var d=c.key,e=c.relatedModel, | ||
i=c.collectionType,g,j,n,m;if(b[d]){g=f.result(b,d);e&&f.isString(e)&&(e=eval(e));i&&f.isString(i)&&(i=eval(i));j=c.options?f.extend({},c.options,a):a;if(c.type===h.Many){if(i&&!i.prototype instanceof l)throw Error("collectionType must inherit from Backbone.Collection");if(g instanceof l)n=g;else{n=i?new i:this._createCollection(e);n.add(g,j)}b[d]=n}else if(c.type===h.One&&e){n=g instanceof o?g:new e(g);b[d]=n}if((m=n)&&!m._proxyCallback){m._proxyCallback=function(){return this._bubbleEvent.call(this, | ||
d,m,arguments)};m.on("all",m._proxyCallback,this)}}},this);return p.set.call(this,b,a)},_bubbleEvent:function(b,a,d){var c=d[0].split(":"),k=c[0],e=d[1],i=-1,g=a._proxyCalls,j;f.size(c)>1&&(j=c[1]);if(a instanceof l&&"change"===k&&e){var h=r(j),m=f.initial(h);(c=a.find(function(a){if(e===a)return true;if(a){var b=a.get(m);if((b instanceof o||b instanceof l)&&e===b)return true;b=a.get(h);return(b instanceof o||b instanceof l)&&e===b}return false}))&&(i=a.indexOf(c))}j=b+(i!==-1?"["+i+"]":"")+(j?"."+ | ||
j:"");d[0]=k+":"+j;if(g){if(i=f.find(g,function(a,b){return j.indexOf(b,j.length-b.length)!==-1}))return this}else g=a._proxyCalls={};g[j]=true;if("change"===k){this._previousAttributes[b]=a._previousAttributes;this.changed[b]=a}this.trigger.apply(this,d);j&&g&&delete g[j];return this},_createCollection:function(b){var a=b;f.isString(a)&&(a=eval(a));if(a&&a.prototype instanceof o){b=new l;b.model=a}else throw Error("type must inherit from Backbone.AssociatedModel");return b},toJSON:function(b){var a, | ||
d;if(!this.visited){this.visited=true;a=p.toJSON.apply(this,arguments);this.relations&&f.each(this.relations,function(c){var h=this.attributes[c.key];if(h){d=h.toJSON(b);a[c.key]=f.isArray(d)?f.compact(d):d}},this);delete this.visited}return a},clone:function(){return new this.constructor(this.toJSON())},getAttr:function(b){var a=this,b=r(b),d,c;if(!(f.size(b)<1)){for(c=0;c<b.length;c++){d=b[c];if(!a)break;a=a instanceof l&&!isNaN(d)?a.at(d):a.attributes[d]}return a}}});var t=/[^\.\[\]]+/g,r=function(b){return b=== | ||
""?[""]:f.isString(b)?b.match(t):b||[]}}).call(this); | ||
(function(){var v=this,g,h,w,m,r,s,z,o,A,B;"undefined"===typeof window?(g=require("underscore"),h=require("backbone"),"undefined"!==typeof exports&&(exports=module.exports=h)):(g=v._,h=v.Backbone);w=h.Model;m=h.Collection;r=w.prototype;s=m.prototype;A=/[\.\[\]]+/g;z="change add remove reset sort destroy".split(" ");B=["reset","sort"];h.Associations={VERSION:"0.5.0"};h.Associations.Many=h.Many="Many";h.Associations.One=h.One="One";o=h.AssociatedModel=h.Associations.AssociatedModel=w.extend({relations:void 0, | ||
_proxyCalls:void 0,get:function(a){var c=r.get.call(this,a);return c?c:this._getAttr.apply(this,arguments)},set:function(a,c,d){var b;if(g.isObject(a)||a==null){b=a;d=c}else{b={};b[a]=c}a=this._set(b,d);this._processPendingEvents();return a},_set:function(a,c){var d,b,n,f,j=this;if(!a)return this;for(d in a){b||(b={});if(d.match(A)){var k=x(d);f=g.initial(k);k=k[k.length-1];f=this.get(f);if(f instanceof o){f=b[f.cid]||(b[f.cid]={model:f,data:{}});f.data[k]=a[d]}}else{f=b[this.cid]||(b[this.cid]={model:this, | ||
data:{}});f.data[d]=a[d]}}if(b)for(n in b){f=b[n];this._setAttr.call(f.model,f.data,c)||(j=false)}else j=this._setAttr.call(this,a,c);return j},_setAttr:function(a,c){var d;c||(c={});if(c.unset)for(d in a)a[d]=void 0;this.parents=this.parents||[];this.relations&&g.each(this.relations,function(b){var d=b.key,f=b.relatedModel,j=b.collectionType,k=b.map,i=this.attributes[d],y=i&&i.idAttribute,e,q,l,p;f&&g.isString(f)&&(f=t(f));j&&g.isString(j)&&(j=t(j));k&&g.isString(k)&&(k=t(k));q=b.options?g.extend({}, | ||
b.options,c):c;if(a[d]){e=g.result(a,d);e=k?k(e):e;if(b.type===h.Many){if(j&&!j.prototype instanceof m)throw Error("collectionType must inherit from Backbone.Collection");if(e instanceof m)l=e;else if(i){i._deferEvents=true;i.set(e,c);l=i}else{l=j?new j:this._createCollection(f);l.add(e,q)}}else if(b.type===h.One&&f)if(e instanceof o)l=e;else if(i)if(i&&e[y]&&i.get(y)===e[y]){i._deferEvents=true;i._set(e,c);l=i}else l=new f(e,q);else l=new f(e,q);if((p=a[d]=l)&&!p._proxyCallback){p._proxyCallback= | ||
function(){return this._bubbleEvent.call(this,d,p,arguments)};p.on("all",p._proxyCallback,this)}}if(a.hasOwnProperty(d)){b=a[d];f=this.attributes[d];if(b){b.parents=b.parents||[];g.indexOf(b.parents,this)==-1&&b.parents.push(this)}else if(f&&f.parents.length>0)f.parents=g.difference(f.parents,[this])}},this);return r.set.call(this,a,c)},_bubbleEvent:function(a,c,d){var b=d[0].split(":"),n=b[0],f=d[0]=="nested-change",j=d[1],k=d[2],i=-1,h=c._proxyCalls,e,q=g.indexOf(z,n)!==-1;if(!f){g.size(b)>1&&(e= | ||
b[1]);g.indexOf(B,n)!==-1&&(k=j);if(c instanceof m&&q&&j){var l=x(e),p=g.initial(l);(b=c.find(function(a){if(j===a)return true;if(!a)return false;var b=a.get(p);if((b instanceof o||b instanceof m)&&j===b)return true;b=a.get(l);if((b instanceof o||b instanceof m)&&j===b||b instanceof m&&k&&k===b)return true}))&&(i=c.indexOf(b))}e=a+(i!==-1&&(n==="change"||e)?"["+i+"]":"")+(e?"."+e:"");if(/\[\*\]/g.test(e))return this;b=e.replace(/\[\d+\]/g,"[*]");i=[];i.push.apply(i,d);i[0]=n+":"+e;h=c._proxyCalls= | ||
h||{};if(this._isEventAvailable.call(this,h,e))return this;h[e]=true;if("change"===n){this._previousAttributes[a]=c._previousAttributes;this.changed[a]=c}this.trigger.apply(this,i);"change"===n&&this.get(e)!=d[2]&&this.trigger.apply(this,["nested-change",e,d[1]]);h&&e&&delete h[e];if(e!==b){i[0]=n+":"+b;this.trigger.apply(this,i)}return this}},_isEventAvailable:function(a,c){return g.find(a,function(a,b){return c.indexOf(b,c.length-b.length)!==-1})},_createCollection:function(a){var c=a;g.isString(c)&& | ||
(c=t(c));if(c&&c.prototype instanceof o){a=new m;a.model=c}else throw Error("type must inherit from Backbone.AssociatedModel");return a},_processPendingEvents:function(){if(!this.visited){this.visited=true;this._deferEvents=false;g.each(this._pendingEvents,function(a){a.c.trigger.apply(a.c,a.a)});this._pendingEvents=[];g.each(this.relations,function(a){(a=this.attributes[a.key])&&a._processPendingEvents()},this);delete this.visited}},trigger:function(a){if(this._deferEvents){this._pendingEvents=this._pendingEvents|| | ||
[];this._pendingEvents.push({c:this,a:arguments})}else r.trigger.apply(this,arguments)},toJSON:function(a){var c,d;if(!this.visited){this.visited=true;c=r.toJSON.apply(this,arguments);this.relations&&g.each(this.relations,function(b){var h=this.attributes[b.key];if(h){d=h.toJSON(a);c[b.key]=g.isArray(d)?g.compact(d):d}},this);delete this.visited}return c},clone:function(){return new this.constructor(this.toJSON())},_getAttr:function(a){var c=this,a=x(a),d,b;if(!(g.size(a)<1)){for(b=0;b<a.length;b++){d= | ||
a[b];if(!c)break;c=c instanceof m?isNaN(d)?void 0:c.at(d):c.attributes[d]}return c}}});var C=/[^\.\[\]]+/g,x=function(a){return a===""?[""]:g.isString(a)?a.match(C):a||[]},t=function(a){return g.reduce(a.split("."),function(a,d){return a[d]},v)},D=function(a,c,d){var b;g.find(a,function(a){if(b=g.find(a.relations,function(b){return a.get(b.key)===c},this))return true},this);return b&&b.map?b.map(d):d},u={};g.each(["set","remove","reset"],function(a){u[a]=m.prototype[a];s[a]=function(c,d){this.model.prototype instanceof | ||
o&&this.parents&&(arguments[0]=D(this.parents,this,c));return u[a].apply(this,arguments)}});u.trigger=s.trigger;s.trigger=function(a){if(this._deferEvents){this._pendingEvents=this._pendingEvents||[];this._pendingEvents.push({c:this,a:arguments})}else u.trigger.apply(this,arguments)};s._processPendingEvents=o.prototype._processPendingEvents}).call(this); |
// | ||
// Backbone-associations.js 0.4.2 | ||
// Backbone-associations.js 0.5.0 | ||
// | ||
@@ -22,8 +22,11 @@ // (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd. | ||
var _, Backbone, BackboneModel, BackboneCollection, ModelProto, | ||
defaultEvents, AssociatedModel, pathChecker; | ||
CollectionProto, defaultEvents, AssociatedModel, pathChecker, | ||
collectionEvents; | ||
if (typeof require !== 'undefined') { | ||
if (typeof window === 'undefined') { | ||
_ = require('underscore'); | ||
Backbone = require('backbone'); | ||
exports = module.exports = Backbone; | ||
if (typeof exports !== 'undefined') { | ||
exports = module.exports = Backbone; | ||
} | ||
} else { | ||
@@ -37,8 +40,13 @@ _ = root._; | ||
ModelProto = BackboneModel.prototype; | ||
CollectionProto = BackboneCollection.prototype; | ||
pathChecker = /[\.\[\]]+/g; | ||
// Built-in Backbone `events`. | ||
defaultEvents = ["change", "add", "remove", "reset", "destroy", | ||
"sync", "error", "sort", "request"]; | ||
defaultEvents = ["change", "add", "remove", "reset", "sort", "destroy"]; | ||
collectionEvents = ["reset", "sort"]; | ||
Backbone.Associations = { | ||
VERSION:"0.5.0" | ||
}; | ||
// Backbone.AssociatedModel | ||
@@ -48,6 +56,6 @@ // -------------- | ||
//Add `Many` and `One` relations to Backbone Object. | ||
Backbone.Many = "Many"; | ||
Backbone.One = "One"; | ||
Backbone.Associations.Many = Backbone.Many = "Many"; | ||
Backbone.Associations.One = Backbone.One = "One"; | ||
// Define `AssociatedModel` (Extends Backbone.Model). | ||
AssociatedModel = Backbone.AssociatedModel = BackboneModel.extend({ | ||
AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({ | ||
// Define relations with Associated Model. | ||
@@ -62,3 +70,3 @@ relations:undefined, | ||
var obj = ModelProto.get.call(this, attr); | ||
return obj ? obj : this.getAttr.apply(this, arguments); | ||
return obj ? obj : this._getAttr.apply(this, arguments); | ||
}, | ||
@@ -68,3 +76,3 @@ | ||
set:function (key, value, options) { | ||
var attributes, attr, modelMap, modelId, obj, result = this; | ||
var attributes, result; | ||
// Duplicate backbone's behavior to allow separate key/value parameters, | ||
@@ -79,2 +87,12 @@ // instead of a single 'attributes' object. | ||
} | ||
result = this._set(attributes, options); | ||
// Trigger events which have been blocked until the entire object graph is updated. | ||
this._processPendingEvents(); | ||
return result; | ||
}, | ||
// Works with an attribute hash and options + fully qualified paths | ||
_set:function (attributes, options) { | ||
var attr, modelMap, modelId, obj, result = this; | ||
if (!attributes) return this; | ||
@@ -85,3 +103,4 @@ for (attr in attributes) { | ||
if (attr.match(pathChecker)) { | ||
var pathTokens = getPathArray(attr), initials = _.initial(pathTokens), last = pathTokens[pathTokens.length - 1], | ||
var pathTokens = getPathArray(attr), initials = _.initial(pathTokens), | ||
last = pathTokens[pathTokens.length - 1], | ||
parentModel = this.get(initials); | ||
@@ -97,11 +116,14 @@ if (parentModel instanceof AssociatedModel) { | ||
} | ||
if (modelMap) { | ||
for (modelId in modelMap) { | ||
obj = modelMap[modelId]; | ||
this.setAttr.call(obj.model, obj.data, options) || (result = false); | ||
this._setAttr.call(obj.model, obj.data, options) || (result = false); | ||
} | ||
} else { | ||
return this.setAttr.call(this, attributes, options); | ||
result = this._setAttr.call(this, attributes, options); | ||
} | ||
return result; | ||
}, | ||
@@ -113,3 +135,3 @@ | ||
// It also bubbles up child events to the parent. | ||
setAttr:function (attributes, options) { | ||
_setAttr:function (attributes, options) { | ||
var attr; | ||
@@ -119,2 +141,3 @@ // Extract attributes and options. | ||
if (options.unset) for (attr in attributes) attributes[attr] = void 0; | ||
this.parents = this.parents || []; | ||
@@ -127,13 +150,22 @@ if (this.relations) { | ||
collectionType = relation.collectionType, | ||
map = relation.map, | ||
currVal = this.attributes[relationKey], | ||
idKey = currVal && currVal.idAttribute, | ||
val, relationOptions, data, relationValue; | ||
//Get class if relation and map is stored as a string. | ||
relatedModel && _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel)); | ||
collectionType && _.isString(collectionType) && (collectionType = map2Scope(collectionType)); | ||
map && _.isString(map) && (map = map2Scope(map)); | ||
// Merge in `options` specific to this relation. | ||
relationOptions = relation.options ? _.extend({}, relation.options, options) : options; | ||
if (attributes[relationKey]) { | ||
//Get value of attribute with relation key in `val`. | ||
// Get value of attribute with relation key in `val`. | ||
val = _.result(attributes, relationKey); | ||
// Get class if relation is stored as a string. | ||
relatedModel && _.isString(relatedModel) && (relatedModel = eval(relatedModel)); | ||
collectionType && _.isString(collectionType) && (collectionType = eval(collectionType)); | ||
// Merge in `options` specific to this relation. | ||
relationOptions = relation.options ? _.extend({}, relation.options, options) : options; | ||
// Map `val` if a transformation function is provided. | ||
val = map ? map(val) : val; | ||
// If `relation.type` is `Backbone.Many`, | ||
// create `Backbone.Collection` with passed data and perform Backbone `set`. | ||
// Create `Backbone.Collection` with passed data and perform Backbone `set`. | ||
if (relation.type === Backbone.Many) { | ||
@@ -147,14 +179,42 @@ // `collectionType` of defined `relation` should be instance of `Backbone.Collection`. | ||
data = val; | ||
attributes[relationKey] = data; | ||
} else { | ||
data = collectionType ? new collectionType() : this._createCollection(relatedModel); | ||
data.add(val, relationOptions); | ||
attributes[relationKey] = data; | ||
// Create a new collection | ||
if (!currVal) { | ||
data = collectionType ? new collectionType() : this._createCollection(relatedModel); | ||
data.add(val, relationOptions); | ||
} else { | ||
// Setting this flag will prevent events from firing immediately. That way clients | ||
// will not get events until the entire object graph is updated. | ||
currVal._deferEvents = true; | ||
// Use Backbone.Collection's smart `set` method | ||
currVal.set(val, options); | ||
data = currVal; | ||
} | ||
} | ||
} else if (relation.type === Backbone.One && relatedModel) { | ||
data = val instanceof AssociatedModel ? val : new relatedModel(val); | ||
attributes[relationKey] = data; | ||
if (val instanceof AssociatedModel) { | ||
data = val; | ||
} else { | ||
//Create a new model | ||
if (!currVal) { | ||
data = new relatedModel(val, relationOptions); | ||
} else { | ||
//Is the passed in data for the same key? | ||
if (currVal && val[idKey] && currVal.get(idKey) === val[idKey]) { | ||
// Setting this flag will prevent events from firing immediately. That way clients | ||
// will not get events until the entire object graph is updated. | ||
currVal._deferEvents = true; | ||
// Perform the traditional `set` operation | ||
currVal._set(val, options); | ||
data = currVal; | ||
} else { | ||
data = new relatedModel(val, relationOptions); | ||
} | ||
} | ||
} | ||
} | ||
attributes[relationKey] = data; | ||
relationValue = data; | ||
@@ -172,6 +232,18 @@ | ||
} | ||
//Distinguish between the value of undefined versus a set no-op | ||
if (attributes.hasOwnProperty(relationKey)) { | ||
//Maintain reverse pointers - a.k.a parents | ||
var updated = attributes[relationKey]; | ||
var original = this.attributes[relationKey]; | ||
if (updated) { | ||
updated.parents = updated.parents || []; | ||
(_.indexOf(updated.parents, this) == -1) && updated.parents.push(this); | ||
} else if (original && original.parents.length > 0) { | ||
original.parents = _.difference(original.parents, [this]); | ||
} | ||
} | ||
}, this); | ||
} | ||
// Return results for `BackboneModel.set`. | ||
return ModelProto.set.call(this, attributes, options); | ||
return ModelProto.set.call(this, attributes, options); | ||
}, | ||
@@ -183,11 +255,24 @@ // Bubble-up event to `parent` Model | ||
eventType = opt[0], | ||
catch_all = args[0] == "nested-change", | ||
eventObject = args[1], | ||
colObject = args[2], | ||
indexEventObject = -1, | ||
_proxyCalls = relationValue._proxyCalls, | ||
cargs, | ||
eventPath, | ||
eventAvailable; | ||
basecolEventPath, | ||
isDefaultEvent = _.indexOf(defaultEvents, eventType) !== -1; | ||
//Short circuit the listen in to the nested-graph event | ||
if (catch_all) return; | ||
// Change the event name to a fully qualified path. | ||
_.size(opt) > 1 && (eventPath = opt[1]); | ||
if (_.indexOf(collectionEvents, eventType) !== -1) { | ||
colObject = eventObject; | ||
} | ||
// Find the specific object in the collection which has changed. | ||
if (relationValue instanceof BackboneCollection && "change" === eventType && eventObject) { | ||
if (relationValue instanceof BackboneCollection && isDefaultEvent && eventObject) { | ||
var pathTokens = getPathArray(eventPath), | ||
@@ -198,32 +283,43 @@ initialTokens = _.initial(pathTokens), colModel; | ||
if (eventObject === model) return true; | ||
if (model) { | ||
var changedModel = model.get(initialTokens); | ||
if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) && eventObject === changedModel) return true; | ||
changedModel = model.get(pathTokens); | ||
return ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) && eventObject === changedModel); | ||
} | ||
return false; | ||
if (!model) return false; | ||
var changedModel = model.get(initialTokens); | ||
if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) | ||
&& eventObject === changedModel) | ||
return true; | ||
changedModel = model.get(pathTokens); | ||
if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) | ||
&& eventObject === changedModel) | ||
return true; | ||
if (changedModel instanceof BackboneCollection && colObject | ||
&& colObject === changedModel) | ||
return true; | ||
}); | ||
colModel && (indexEventObject = relationValue.indexOf(colModel)); | ||
} | ||
// Manipulate `eventPath`. | ||
eventPath = relationKey + (indexEventObject !== -1 ? | ||
eventPath = relationKey + ((indexEventObject !== -1 && (eventType === "change" || eventPath)) ? | ||
"[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : ""); | ||
args[0] = eventType + ":" + eventPath; | ||
// Short circuit collection * events | ||
if (/\[\*\]/g.test(eventPath)) return this; | ||
basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]'); | ||
cargs = []; | ||
cargs.push.apply(cargs, args); | ||
cargs[0] = eventType + ":" + eventPath; | ||
// If event has been already triggered as result of same source `eventPath`, | ||
// no need to re-trigger event to prevent cycle. | ||
if (_proxyCalls) { | ||
eventAvailable = _.find(_proxyCalls, function (value, eventKey) { | ||
return eventPath.indexOf(eventKey, eventPath.length - eventKey.length) !== -1; | ||
}); | ||
if (eventAvailable) return this; | ||
} else { | ||
_proxyCalls = relationValue._proxyCalls = {}; | ||
} | ||
_proxyCalls = relationValue._proxyCalls = (_proxyCalls || {}); | ||
if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this; | ||
// Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`. | ||
_proxyCalls[eventPath] = true; | ||
//Set up previous attributes correctly. Backbone v0.9.10 upwards... | ||
// Set up previous attributes correctly. | ||
if ("change" === eventType) { | ||
@@ -235,16 +331,34 @@ this._previousAttributes[relationKey] = relationValue._previousAttributes; | ||
// Bubble up event to parent `model` with new changed arguments. | ||
this.trigger.apply(this, args); | ||
this.trigger.apply(this, cargs); | ||
//Only fire for change. Not change:attribute | ||
if ("change" === eventType && this.get(eventPath) != args[2]) { | ||
this.trigger.apply(this, ["nested-change", eventPath, args[1]]); | ||
} | ||
// Remove `eventPath` from `_proxyCalls`, | ||
// if `eventPath` and `_proxCalls` are available, | ||
// if `eventPath` and `_proxyCalls` are available, | ||
// which allow event to be triggered on for next operation of `set`. | ||
if (eventPath && _proxyCalls) { | ||
delete _proxyCalls[eventPath]; | ||
if (_proxyCalls && eventPath) delete _proxyCalls[eventPath]; | ||
// Create a collection modified event with wild-card | ||
if (eventPath !== basecolEventPath) { | ||
cargs[0] = eventType + ":" + basecolEventPath; | ||
this.trigger.apply(this, cargs); | ||
} | ||
return this; | ||
}, | ||
// Has event been fired from this source. Used to prevent event recursion in cyclic graphs | ||
_isEventAvailable:function (_proxyCalls, path) { | ||
return _.find(_proxyCalls, function (value, eventKey) { | ||
return path.indexOf(eventKey, path.length - eventKey.length) !== -1; | ||
}); | ||
}, | ||
// Returns New `collection` of type `relation.relatedModel`. | ||
_createCollection:function (type) { | ||
var collection, relatedModel = type; | ||
_.isString(relatedModel) && (relatedModel = eval(relatedModel)); | ||
_.isString(relatedModel) && (relatedModel = map2Scope(relatedModel)); | ||
// Creates new `Backbone.Collection` and defines model class. | ||
@@ -259,2 +373,39 @@ if (relatedModel && relatedModel.prototype instanceof AssociatedModel) { | ||
}, | ||
// Process all pending events after the entire object graph has been updated | ||
_processPendingEvents:function () { | ||
if (!this.visited) { | ||
this.visited = true; | ||
this._deferEvents = false; | ||
// Trigger all pending events | ||
_.each(this._pendingEvents, function (e) { | ||
e.c.trigger.apply(e.c, e.a); | ||
}); | ||
this._pendingEvents = []; | ||
// Traverse down the object graph and call process pending events on sub-trees | ||
_.each(this.relations, function (relation) { | ||
var val = this.attributes[relation.key]; | ||
val && val._processPendingEvents(); | ||
}, this); | ||
delete this.visited; | ||
} | ||
}, | ||
// Override trigger to defer events in the object graph. | ||
trigger:function (name) { | ||
// Defer event processing | ||
if (this._deferEvents) { | ||
this._pendingEvents = this._pendingEvents || []; | ||
// Maintain a queue of pending events to trigger after the entire object graph is updated. | ||
this._pendingEvents.push({c:this, a:arguments}); | ||
} else { | ||
ModelProto.trigger.apply(this, arguments); | ||
} | ||
}, | ||
// The JSON representation of the model. | ||
@@ -288,4 +439,4 @@ toJSON:function (options) { | ||
//Navigate the path to the leaf object in the path to query for the attribute value | ||
getAttr:function (path) { | ||
// Navigate the path to the leaf object in the path to query for the attribute value | ||
_getAttr:function (path) { | ||
@@ -302,3 +453,5 @@ var result = this, | ||
//Navigate the path to get to the result | ||
result = result instanceof BackboneCollection && (!isNaN(key)) ? result.at(key) : result.attributes[key]; | ||
result = result instanceof BackboneCollection | ||
? (isNaN(key) ? undefined : result.at(key)) | ||
: result.attributes[key]; | ||
} | ||
@@ -315,3 +468,60 @@ return result; | ||
return _.isString(path) ? (path.match(delimiters)) : path || []; | ||
} | ||
}).call(this); | ||
}; | ||
var map2Scope = function (path) { | ||
return _.reduce(path.split('.'), function (memo, elem) { | ||
return memo[elem] | ||
}, root); | ||
}; | ||
//Infer the relation from the collection's parents and find the appropriate map for the passed in `models` | ||
var map2models = function (parents, target, models) { | ||
var relation; | ||
//Iterate over collection's parents | ||
_.find(parents, function (parent) { | ||
//Iterate over relations | ||
relation = _.find(parent.relations, function (rel) { | ||
return parent.get(rel.key) === target; | ||
}, this); | ||
if (relation) return true;//break; | ||
}, this); | ||
//If we found a relation and it has a mapping function | ||
if (relation && relation.map) { | ||
return relation.map(models) | ||
} | ||
return models; | ||
}; | ||
var proxies = {}; | ||
// Proxy Backbone collection methods | ||
_.each(['set', 'remove', 'reset'], function (method) { | ||
proxies[method] = BackboneCollection.prototype[method]; | ||
CollectionProto[method] = function (models, options) { | ||
//Short-circuit if this collection doesn't hold `AssociatedModels` | ||
if (this.model.prototype instanceof AssociatedModel && this.parents) { | ||
//Find a map function if available and perform a transformation | ||
arguments[0] = map2models(this.parents, this, models); | ||
} | ||
return proxies[method].apply(this, arguments); | ||
} | ||
}); | ||
// Override trigger to defer events in the object graph. | ||
proxies['trigger'] = CollectionProto['trigger']; | ||
CollectionProto['trigger'] = function (name) { | ||
if (this._deferEvents) { | ||
this._pendingEvents = this._pendingEvents || []; | ||
// Maintain a queue of pending events to trigger after the entire object graph is updated. | ||
this._pendingEvents.push({c:this, a:arguments}); | ||
} else { | ||
proxies['trigger'].apply(this, arguments); | ||
} | ||
}; | ||
// Attach process pending event functionality on collections as well. Re-use from `AssociatedModel` | ||
CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents; | ||
}).call(this); |
{ | ||
"name":"backbone-associations", | ||
"version":"0.4.2", | ||
"version":"0.5.0", | ||
"main":"./backbone-associations.js", | ||
@@ -5,0 +5,0 @@ "dependencies":{ |
{ | ||
"name":"backbone-associations", | ||
"description":"Create lightweight object graphs with Backbone models", | ||
"description":"Create object hierarchies with Backbone models. Respond to hierarchy changes using regular Backbone events", | ||
"url":"https://github.com/dhruvaray/backbone-associations/", | ||
"keywords":["Backbone", "association", "associated", "nested", "model", "cyclic graph", "qualified event paths"], | ||
"homepage":"http://dhruvaray.github.io/backbone-associations/", | ||
"keywords":["Backbone", "association", "associated", "nested", "model", "cyclic graph", "qualified event paths","relational","relations"], | ||
"author":["Dhruva Ray", "Jaynti Kanani"], | ||
@@ -14,2 +15,13 @@ "dependencies":{ | ||
}, | ||
"jam": { | ||
"dependencies": { | ||
"underscore":">=1.4.3", | ||
"backbone":">=0.9.10" | ||
}, | ||
"main":"backbone-associations.js", | ||
"include": [ | ||
"README.md", | ||
"CHANGELOG.md" | ||
] | ||
}, | ||
"scripts":{ | ||
@@ -19,3 +31,21 @@ "test":"phantomjs test/lib/runner.js test/test-suite.html?noglobals=true" | ||
"main":"backbone-associations.js", | ||
"version":"0.4.2" | ||
} | ||
"version":"0.5.0", | ||
"repository":"git://github.com/dhruvaray/backbone-associations.git", | ||
"licenses": [ | ||
{ | ||
"type": "MIT", | ||
"url": "https://github.com/dhruvaray/backbone-associations/blob/master/LICENSE.txt" | ||
} | ||
], | ||
"contributors": [ | ||
{ | ||
"name": "Dhruva Ray", | ||
"web": "https://github.com/dhruvaray" | ||
}, | ||
{ | ||
"name": "Jaynti Kanani", | ||
"web": "https://github.com/jdkanani" | ||
} | ||
], | ||
"github": "https://github.com/dhruvaray/backbone-associations/" | ||
} |
606
README.md
@@ -1,607 +0,7 @@ | ||
# Backbone-associations | ||
Backbone-associations provides a way of specifying 1:1 and 1:N relationships between Backbone models. Additionally, parent model instances (and objects extended from `Backbone.Events`) can listen in to CRUD events initiated on any children - in the object graph - by providing an appropriately qualified event path name. It aims to provide a clean implementation which is easy to understand and extend. It is [performant](#performance) for CRUD operations - even on deeply nested object graphs - and uses a low memory footprint. Web applications leveraging the client-side-MVC architectural style will benefit by using `backbone-associations` to define and manipulate client object graphs. | ||
Associations allows Backbone applications to model 1:1 & 1:N associations between application models and Collections. More importantly, applications can listen to any kind of change (change, add, remove, reset, destroy) in this hierarchy using standard Backbone events and respond to them. (views can re-render for example). The implementation strives to be tiny (2.2KB), easy-to-understand, light-weight and highly performant. | ||
It comes with | ||
* The [annotated](http://dhruvaray.github.com/backbone-associations/docs/backbone-associations.html) source code. | ||
* An online [test suite](http://dhruvaray.github.com/backbone-associations/test/test-suite.html) which includes backbone test cases run with `AssociatedModel`s. | ||
* Performance [tests](http://dhruvaray.github.com/backbone-associations/test/speed-comparison.html). | ||
For features, performance #s, API documentation, tutorials and recipes, please visit : | ||
It was originally born out of a need to provide a simpler and speedier implementation of [Backbone-relational](https://github.com/PaulUithol/Backbone-relational/) | ||
http://dhruvaray.github.io/backbone-associations/ | ||
## Contents | ||
* [Download](#download) | ||
* [Installation](#installation) | ||
* [Specifying Associations](#associations) | ||
* [Tutorial : Defining a graph of `AssociatedModel` relationships](#tutorial-associationsdef) | ||
* [Eventing with Associations](#eventing) | ||
* [Tutorial : Eventing with a graph of `AssociatedModel` objects](#tutorial-eventing) | ||
* [Perform set and get operations with fully qualified paths](#paths) | ||
* [Pitfalls](#pitfalls) | ||
* [Performance Comparison](#performance) | ||
* [Change Log](#changelog) | ||
## <a name="download"/>Download | ||
* [Production version - 0.4.2](http://dhruvaray.github.com/backbone-associations/backbone-associations-min.js) (1.44K packed and gzipped) | ||
* [Development version - 0.4.2](http://dhruvaray.github.com/backbone-associations/backbone-associations.js) | ||
* [Edge version : ] (https://raw.github.com/dhruvaray/backbone-associations/master/backbone-associations.js)[![Build Status](https://travis-ci.org/dhruvaray/backbone-associations.png?branch=master)](https://travis-ci.org/dhruvaray/backbone-associations) | ||
## <a name="installation"/>Installation | ||
Backbone-associations depends on [backbone](https://github.com/documentcloud/backbone) (and thus on [underscore](https://github.com/documentcloud/underscore)). Include Backbone-associations right after Backbone and Underscore: | ||
```html | ||
<script type="text/javascript" src="./js/underscore.js"></script> | ||
<script type="text/javascript" src="./js/backbone.js"></script> | ||
<script type="text/javascript" src="./js/backbone-associations.js"></script> | ||
``` | ||
Backbone-associations works with Backbone v0.9.10. Underscore v1.4.3 upwards is supported. | ||
## <a name="associations"/>Specifying Associations | ||
Each `Backbone.AssociatedModel` can contain an array of `relations`. Each relation defines a `relatedModel`, `key`, `type` and (optionally) `collectionType`. This can be easily understood by some examples. | ||
### Specifying One-to-One Relationship | ||
```javascript | ||
var Employee = Backbone.AssociatedModel.extend({ | ||
relations: [ | ||
{ | ||
type: Backbone.One, //nature of the relationship | ||
key: 'manager', // attribute of Employee | ||
relatedModel: 'Employee' //AssociatedModel for attribute key | ||
} | ||
], | ||
defaults: { | ||
age : 0, | ||
fname : "", | ||
lname : "", | ||
manager : null | ||
} | ||
}); | ||
```` | ||
### Specifying One-to-Many Relationship | ||
```javascript | ||
var Location = Backbone.AssociatedModel.extend({ | ||
defaults: { | ||
add1 : "", | ||
add2 : null, | ||
zip : "", | ||
state : "" | ||
} | ||
}); | ||
var Project = Backbone.AssociatedModel.extend({ | ||
relations: [ | ||
{ | ||
type: Backbone.Many,//nature of the relation | ||
key: 'locations', //attribute of Project | ||
relatedModel:Location //AssociatedModel for attribute key | ||
} | ||
], | ||
defaults: { | ||
name : "", | ||
number : 0, | ||
locations : [] | ||
} | ||
}); | ||
``` | ||
#### Valid values for | ||
##### relatedModel | ||
A string (which can be resolved to an object type on the global scope), or a reference to a `Backbone.AssociatedModel` type. | ||
##### key | ||
A string which references an attribute name on `relatedModel`. | ||
##### type : `Backbone.One` or `Backbone.Many` | ||
Used for specifying one-to-one or one-to-many relationships. | ||
##### collectionType (optional) : | ||
A string (which can be resolved to an object type on the global scope), or a reference to a `Backbone.Collection` type. Determine the type of collections used for a `Many` relation. | ||
## <a name="tutorial-associationsdef"/> Tutorial : Defining a graph of `AssociatedModel` relationships | ||
This tutorial demonstrates how to convert the following relationship graph into an `AssociatedModels` representation | ||
![cd_example](http://dhruvaray.github.com/backbone-associations/docs/img/cd_example.png) | ||
This image was generated via [code](https://github.com/dhruvaray/backbone-associations/blob/master/docs/cd_example.tex). | ||
````javascript | ||
var Location = Backbone.AssociatedModel.extend({ | ||
defaults:{ | ||
add1:"", | ||
add2:null, | ||
zip:"", | ||
state:"" | ||
} | ||
}); | ||
var Project = Backbone.AssociatedModel.extend({ | ||
relations:[ | ||
{ | ||
type:Backbone.Many, | ||
key:'locations', | ||
relatedModel:Location | ||
} | ||
], | ||
defaults:{ | ||
name:"", | ||
number:0, | ||
locations:[] | ||
} | ||
}); | ||
var Department = Backbone.AssociatedModel.extend({ | ||
relations:[ | ||
{ | ||
type:Backbone.Many, | ||
key:'controls', | ||
relatedModel:Project | ||
}, | ||
{ | ||
type:Backbone.Many, | ||
key:'locations', | ||
relatedModel:Location | ||
} | ||
], | ||
defaults:{ | ||
name:'', | ||
locations:[], | ||
number:-1, | ||
controls:[] | ||
} | ||
}); | ||
var Dependent = Backbone.AssociatedModel.extend({ | ||
validate:function (attr) { | ||
return (attr.sex && attr.sex != "M" && attr.sex != "F") ? "invalid sex value" : undefined; | ||
}, | ||
defaults:{ | ||
fname:'', | ||
lname:'', | ||
sex:'F', //{F,M} | ||
age:0, | ||
relationship:'S' //Values {C=Child, P=Parents} | ||
} | ||
}); | ||
var Employee = Backbone.AssociatedModel.extend({ | ||
relations:[ | ||
{ | ||
type:Backbone.One, | ||
key:'works_for', | ||
relatedModel:Department | ||
}, | ||
{ | ||
type:Backbone.Many, | ||
key:'dependents', | ||
relatedModel:Dependent | ||
}, | ||
{ | ||
type:Backbone.One, | ||
key:'manager', | ||
relatedModel:'Employee' | ||
} | ||
], | ||
validate:function (attr) { | ||
return (attr.sex && attr.sex != "M" && attr.sex != "F") ? "invalid sex value" : undefined; | ||
}, | ||
defaults:{ | ||
sex:'M', //{F,M} | ||
age:0, | ||
fname:"", | ||
lname:"", | ||
works_for:{}, | ||
dependents:[], | ||
manager:null | ||
} | ||
}); | ||
```` | ||
## <a name="eventing"/>Eventing with `AssociatedModels` | ||
CRUD operations on AssociatedModels trigger the appropriate Backbone [system events](http://backbonejs.org/#Events-catalog). However, because we are working with an object graph, the event name now contains the fully qualified path from the source of the event to the receiver of the event. The remaining event arguments are identical to the Backbone event arguments and vary based on [event type](http://backbonejs.org/#Events-catalog). | ||
An update like this | ||
````javascript | ||
emp.get('works_for').get("locations").at(0).set('zip', 94403); | ||
```` | ||
can be listened to at various levels by spelling out the appropriate path | ||
````javascript | ||
emp.on('change:works_for.locations[0].zip', callback_function); | ||
emp.get('works_for').on('change:locations[0].zip', callback_function); | ||
emp.get('works_for').get('locations').at(0).on('change:zip', callback_function); | ||
```` | ||
With backbone v0.9.9 onwards, another object can also listen in to events like this | ||
````javascript | ||
var listener = {}; | ||
_.extend(listener, Backbone.Events); | ||
listener.listenTo(emp, 'change:works_for.locations[0].zip', callback_function); | ||
listener.listenTo(emp.get('works_for'), 'change:locations[0].zip', callback_function); | ||
listener.listenTo(emp.get('works_for').get('locations').at(0), 'change:zip', callback_function); | ||
```` | ||
A detailed example is provided below to illustrate the behavior for other event types as well as the appropriate usage of the Backbone [change-related methods](http://backbonejs.org/#Model-hasChanged) used in callbacks. | ||
## <a name="tutorial-eventing"/> Tutorial : Eventing with a graph of `AssociatedModel` objects | ||
This tutorial demonstrates the usage of eventing and change-related methods with `AssociatedModels` | ||
#### Setup of relationships between `AssociatedModel` instances | ||
````javascript | ||
emp = new Employee({ | ||
fname:"John", | ||
lname:"Smith", | ||
age:21, | ||
sex:"M" | ||
}); | ||
child1 = new Dependent({ | ||
fname:"Jane", | ||
lname:"Smith", | ||
sex:"F", | ||
relationship:"C" | ||
}); | ||
child2 = new Dependent({ | ||
fname:"Barbara", | ||
lname:"Ruth", | ||
sex:"F", | ||
relationship:"C" | ||
}); | ||
parent1 = new Dependent({ | ||
fname:"Edgar", | ||
lname:"Smith", | ||
sex:"M", | ||
relationship:"P" | ||
}); | ||
loc1 = new Location({ | ||
add1:"P.O Box 3899", | ||
zip:"94404", | ||
state:"CA" | ||
}); | ||
loc2 = new Location({ | ||
add1:"P.O Box 4899", | ||
zip:"95502", | ||
state:"CA" | ||
}); | ||
project1 = new Project({ | ||
name:"Project X", | ||
number:"2" | ||
}); | ||
project2 = new Project({ | ||
name:"Project Y", | ||
number:"2" | ||
}); | ||
project2.get("locations").add(loc2); | ||
project1.get("locations").add(loc1); | ||
dept1 = new Department({ | ||
name:"R&D", | ||
number:"23" | ||
}); | ||
dept1.set({locations:[loc1, loc2]}); | ||
dept1.set({controls:[project1, project2]}); | ||
emp.set({"dependents":[child1, parent1]}); | ||
```` | ||
#### Assign `Associated Model` instances to other properties | ||
````javascript | ||
emp.on('change', function () { | ||
console.log("Fired emp > change..."); | ||
//emp.hasChanged() === true; | ||
//emp.hasChanged("works_for") === true; | ||
}); | ||
emp.on('change:works_for', function () { | ||
console.log("Fired emp > change:works_for..."); | ||
var changed = emp.changedAttributes(); | ||
//changed['works_for'].toJSON() equals emp.get("works_for").toJSON() | ||
//emp.previousAttributes()['works_for'].get('name') === ""); | ||
//emp.previousAttributes()['works_for'].get('number') === -1; | ||
//emp.previousAttributes()['works_for'].get('locations').length === 0; | ||
//emp.previousAttributes()['works_for'].get('controls').length === 0; | ||
}); | ||
emp.set({works_for:dept1}); | ||
//Console log | ||
//Fired emp > change:works_for... | ||
//Fired emp > change... | ||
```` | ||
#### Update attributes of `AssociatedModel` instances | ||
````javascript | ||
//Remove event handlers. Can also use backbone 0.9.9+ once API (on the previous emp event handlers) | ||
emp.off() | ||
emp.get('works_for').on('change', function () { | ||
console.log("Fired emp.works_for > change..."); | ||
//emp.get("works_for").hasChanged() === true; | ||
//emp.get("works_for").previousAttributes()["name"] === "R&D"; | ||
}); | ||
emp.get('works_for').on('change:name', function () { | ||
console.log("Fired emp.works_for > change:name..."); | ||
}); | ||
emp.on('change:works_for.name', function () { | ||
console.log("Fired emp > change:works_for.name..."); | ||
//emp.get("works_for").hasChanged() === true; | ||
//emp.hasChanged() === true; | ||
//emp.hasChanged("works_for") === true; | ||
//emp.changedAttributes()['works_for'].toJSON() equals emp.get("works_for").toJSON(); | ||
//emp.get("works_for").previousAttributes()["name"] === "R&D"; | ||
//emp.get("works_for").previous("name") === "R&D"; | ||
}); | ||
emp.on('change:works_for', function () { | ||
console.log("Fired emp > change:works_for..."); | ||
//emp.hasChanged()); | ||
//emp.hasChanged("works_for")); | ||
//emp.changedAttributes()['works_for'].toJSON() equals emp.get("works_for").toJSON(); | ||
//emp.previousAttributes().works_for.name === "R&D"; | ||
}); | ||
emp.get('works_for').set({name:"Marketing"}); | ||
//Console log | ||
//Fired emp.works_for > change:name | ||
//Fired emp > change:works_for.name... | ||
//Fired emp.works_for > change... | ||
//Fired emp > change:works_for... | ||
```` | ||
#### Update an item in a `Collection` of `AssociatedModel`s | ||
````javascript | ||
emp.get('works_for').get('locations').at(0).on('change:zip', function () { | ||
console.log("Fired emp.works_for.locations[0] > change:zip..."); | ||
}); | ||
emp.get('works_for').get('locations').at(0).on('change', function () { | ||
console.log("Fired emp.works_for.locations[0] > change..."); | ||
}); | ||
emp.get('works_for').on('change:locations[0].zip', function () { | ||
console.log("Fired emp.works_for > change:locations[0].zip..."); | ||
}); | ||
emp.get('works_for').on('change:locations[0]', function () { | ||
console.log("Fired emp.works_for > change:locations[0]..."); | ||
}); | ||
emp.on('change:works_for.locations[0].zip', function () { | ||
console.log("Fired emp > change:works_for.locations[0].zip..."); | ||
}); | ||
emp.on('change:works_for.locations[0]', function () { | ||
console.log("Fired emp > change:works_for.locations[0]..."); | ||
}); | ||
emp.on('change:works_for.controls[0].locations[0].zip', function () { | ||
console.log("Fired emp > change:works_for.controls[0].locations[0].zip..."); | ||
}); | ||
emp.on('change:works_for.controls[0].locations[0]', function () { | ||
console.log("Fired emp > change:works_for.controls[0].locations[0]..."); | ||
}); | ||
emp.get('works_for').on('change:controls[0].locations[0].zip', function () { | ||
console.log("Fired emp.works_for > change:controls[0].locations[0].zip..."); | ||
}); | ||
emp.get('works_for').on('change:controls[0].locations[0]', function () { | ||
console.log("Fired emp.works_for > change:controls[0].locations[0]..."); | ||
}); | ||
emp.get('works_for').get("locations").at(0).set('zip', 94403); | ||
//Console log | ||
//Fired emp.works_for > change:controls[0].locations[0]... | ||
//Fired emp.works_for > change:controls[0].locations[0].zip... | ||
//Fired emp.works_for > change:locations[0]... | ||
//Fired emp.works_for > change:locations[0].zip... | ||
//Fired emp > change:works_for.controls[0].locations[0]... | ||
//Fired emp > change:works_for.controls[0].locations[0].zip... | ||
//Fired emp > change:works_for.locations[0]... | ||
//Fired emp > change:works_for.locations[0].zip... | ||
//Fired emp.works_for.locations[0] > change... | ||
//Fired emp.works_for.locations[0].zip > change... | ||
```` | ||
#### Add, remove and reset operations | ||
````javascript | ||
emp.on('add:dependents', function () { | ||
console.log("Fired emp > add:dependents..."); | ||
}); | ||
emp.on('remove:dependents', function () { | ||
console.log("Fired emp > remove:dependents..."); | ||
}); | ||
emp.on('reset:dependents', function () { | ||
console.log("Fired emp > reset:dependents..."); | ||
}); | ||
emp.get('dependents').on('add', function () { | ||
console.log("Fired emp.dependents add..."); | ||
}); | ||
emp.get('dependents').on('remove', function () { | ||
console.log("Fired emp.dependents remove..."); | ||
}); | ||
emp.get('dependents').on('reset', function () { | ||
console.log("Fired emp.dependents reset..."); | ||
}); | ||
emp.get("dependents").add(child2); | ||
emp.get("dependents").remove([child1]); | ||
emp.get("dependents").reset(); | ||
//Console log | ||
//Fired emp.dependents add... | ||
//Fired Fired emp.dependents remove... | ||
//Fired emp.dependents reset... | ||
//Fired emp > add:dependents... | ||
//Fired emp > remove:dependents... | ||
//Fired emp > reset:dependents... | ||
```` | ||
The preceding examples corresponds to this [test case](http://dhruvaray.github.com/backbone-associations/test/test-suite.html?module=Examples). | ||
Other examples can be found in the [test suite](http://dhruvaray.github.com/backbone-associations/test/test-suite.html). | ||
## <a name="paths"/>Retrieve and set data with fully qualified paths | ||
For convenience, it is also possible to retrieve or set data by specifying a path to the destination (of the retrieve or set operation). | ||
````javascript | ||
emp.get('works_for.controls[0].locations[0].zip') //94404 | ||
//Equivalent to emp.get('works_for').get('controls').at(0).get('locations').at(0).get('zip'); | ||
emp.set('works_for.locations[0].zip', 94403); | ||
//Equivalent to emp.get('works_for').get('locations').at(0).set('zip',94403); | ||
```` | ||
## <a name="performance"/>Pitfalls | ||
##### Query the appropriate object to determine change | ||
When assigning a previously created object graph to a property in an associated model, care must be taken to query the appropriate object for the changed properties. | ||
````javascript | ||
dept1 = new Department({ | ||
name:"R&D", | ||
number:"23" | ||
}); | ||
//dept1.hasChanged() === false; | ||
emp.set('works_for', dept1); | ||
```` | ||
Then inside a previously defined `change` event handler | ||
````javascript | ||
emp.on('change:works_for', function () { | ||
//emp.get('works_for').hasChanged() === false; as we query a previously created `dept1` instance | ||
//emp.hasChanged('works_for') === true; as we query emp whose 'works_for' attribute has changed | ||
}); | ||
```` | ||
##### Use unqualified `change` event name with care | ||
This extension makes use of _fully-qualified-event-path names_ to identify the location of the change in the object graph. (And the event arguments would have the changed object or object property). | ||
The unqualified `change` event would work if an entire object graph is being replaced with another. For example | ||
```javascript | ||
emp.on('change', function () { | ||
console.log("emp has changed");//This WILL fire | ||
}); | ||
emp.on('change:works_for', function () { | ||
console.log('emp attribute works_for has changed');//This WILL fire | ||
}); | ||
emp.set('works_for', {name:'Marketing', number:'24'}); | ||
``` | ||
However, if attributes of a nested object are changed, the unqualified `change` event will not fire for objects (and their parents) who have that nested object as their child. | ||
```javascript | ||
emp.on('change', function () { | ||
console.log("emp has changed"); //This will NOT fire | ||
}); | ||
emp.on('change:works_for', function () { | ||
console.log('emp attribute works_for has changed');//This WILL fire | ||
}); | ||
emp.get('works_for').set('name','Marketing'); | ||
``` | ||
Refer to issue [#28](https://github.com/dhruvaray/backbone-associations/issues/28) for a more detailed reasoning. | ||
## <a name="performance"/>Performance Comparison | ||
![Performance](http://dhruvaray.github.com/backbone-associations/docs/img/speed0.4.1.png) | ||
Each operation comprises of n (10, 15, 20, 25, 30) inserts. The chart above compares the performance (time and operations/sec) of the two implementations. (backbone-associations v0.4.1 v/s backbone-relational v0.7.1) | ||
Run tests on your machine configuration instantly [here](http://dhruvaray.github.com/backbone-associations/test/speed-comparison.html) | ||
Write your own test case [here](http://jsperf.com/backbone-associations-speed-suit/3) | ||
## <a name="changelog"/>Change Log | ||
#### Version 0.4.2 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.4.1...v0.4.2) | ||
* Support for backbone 1.0.0. | ||
#### Version 0.4.1 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.4.0...v0.4.1) | ||
* Support for backbone 0.9.10. | ||
* Faster (Non-recursive) implementation of AssociatedModel change-related methods. | ||
#### Version 0.4.0 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.3.1...v0.4.0) | ||
* Ability to perform set and retrieve operations with fully qualified paths. | ||
#### Version 0.3.1 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.3.0...v0.3.1) | ||
* Bug fix for event paths involving collections at multiple levels in the object graph. | ||
* Updated README with class diagram and example for paths involving collections. | ||
#### Version 0.3.0 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.2.0...v0.3.0) | ||
* Added support for fully qualified event "path" names. | ||
* Event arguments and event paths are semantically consistent. | ||
* Now supports both backbone 0.9.9 and 0.9.2. | ||
* New tutorials on usage. (part of README.md) | ||
#### Version 0.2.0 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.1.0...v0.2.0) | ||
Added support for cyclic object graphs. | ||
#### Version 0.1.0 | ||
Initial Backbone-associations release. | ||
@@ -1227,2 +1227,2 @@ // Underscore.js 1.4.4 | ||
}).call(this); | ||
}).call(this); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
23583
2
2
0
879495
27
8