Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

backbone-associations

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

backbone-associations - npm Package Compare versions

Comparing version 0.4.2 to 0.5.0

CHANGELOG.md

18

backbone-associations-min.js

@@ -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/"
}

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc