UNPKG

dtx-backbone-associations

Version:

Create object hierarchies with Backbone models. Respond to hierarchy changes using regular Backbone events

1,395 lines (1,169 loc) 129 kB
$(document).ready(function () { if (!window.console) { window.console = {}; var names = [ 'log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml', 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd' ]; for (var i = 0; i < names.length; ++i) { window.console[ names[i] ] = function () { }; } } var attr , child1, child2, child3, child4 , dept1 , emp , loc1, loc2 , node1, node2, node3 , parent1 , project1, project2 ; var Location = Backbone.AssociatedModel.extend({ defaults:{ id:-1, add1:"", add2:null, zip:"", state:"" }, urlRoot:'/location' }); //location store var locations = new Backbone.Collection([ new Location({id:3, add1:"loc3", state:"AL"}), new Location({id:4, add1:"loc4", state:"VA"}), new Location({id:5, add1:"loc5", state:"CA"}), new Location({id:6, add1:"loc6", state:"IN"}), new Location({id:7, add1:"loc7", state:"NY"}), new Location({id:8, add1:"loc8", state:"NY"}) ]); var map2locs = function (ids) { if (ids instanceof Backbone.Collection) return ids; if (_.isArray(ids) && ids.length > 0) { if (_.isObject(ids[0])) //dummy logic to check whether array has ids or objects return ids; } else { if (_.isObject(ids)) return ids; } ids = _.isArray(ids) ? ids.slice() : [ids]; var result = []; _.each(ids, function (id) { if (!id) return; var mapped = _.find(locations.models, function (m) { if (m.get('id') == id) return m; }); result.push(mapped || id); }); return result; }; var Project = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.Many, key:'locations', relatedModel:Location, map:map2locs } ], defaults:{ name:"", number:0, locations:[] }, urlRoot:'/project' }); var Department = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.Many, key:'controls', relatedModel:Project }, { type:Backbone.Many, key:'locations', relatedModel:Location, map:map2locs } ], defaults:{ name:'', locations:[], number:-1, controls:[] }, urlRoot:'/department' }); 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} }, urlRoot:'/dependent' }); //dept store var store = new Backbone.Collection([new Department({number:99, name:"sales"}), new Department({number:100, name:"admin"})]); var map2dept = function (id) { if (id instanceof Department) return id; if (_.isObject(id)) return id; var found = _.find(store.models, function (m) { return m.get('number') == id }); return found ? found : id; }; Employee = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'works_for', relatedModel:Department, map:map2dept }, { 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 }, urlRoot:'/employee' }); Node = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'parent', relatedModel:'Node' }, { type:Backbone.Many, key:'children', relatedModel:'Node' } ], defaults:{ name:'' } }); module("Backbone.AssociatedModel", { setup:function () { 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" }); child3 = new Dependent({ fname:"Gregory", lname:"Smith", sex:"M", relationship:"C" }); child4 = new Dependent({ fname:"Jane", lname:"Doe", 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", id:"1" }); loc2 = new Location({ add1:"P.O Box 4899", zip:"95502", state:"CA", id:"2" }); 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({"works_for":dept1}); emp.set({"dependents":[child1, parent1]}); } }); test("initialize", 1, function () { equal(emp.get('fname'), 'John', 'name should be John'); }); test("VERSION", 1, function () { ok(Backbone.Associations.VERSION, "Backbone.Associations.VERSION exists"); }); test("SEPARATOR", 13, function () { // Change separator to `~` Backbone.Associations.SEPARATOR = "~"; equal(Backbone.Associations.SEPARATOR, "~", "Backbone.Associations.SEPERATOR should be `~`"); equal(emp.get('dependents[0]~fname'), emp.get('dependents').at(0).get('fname')); equal(emp.get('works_for~controls[0]~locations[0]~zip'), 94404); emp.once('change:works_for~controls[0]~locations[0]', function () { ok(true, "Fired emp change:works_for~controls[0]~locations[0]..."); }); emp.once('change:works_for~controls[*]~locations[*]', function () { ok(true, "Fired emp change:works_for~controls[*]~locations[*]..."); }); emp.once('change:works_for~controls[*]~locations[*]~zip', function () { ok(true, "Fired emp change:works_for~controls[*]~locations[*]~zip..."); }); emp.get('works_for').get("locations").at(0).set('zip', 94403); // 3 // Change separator to `->` Backbone.Associations.SEPARATOR = "->"; equal(Backbone.Associations.SEPARATOR, "->", "Backbone.Associations.SEPERATOR should be `->`"); emp.get('works_for').on('change:locations[*]', function () { ok(true, "Fired emp.works_for change:locations[*]..."); }); emp.get('works_for').on('change:locations[0]->zip', function () { ok(true, "Fired emp.works_for change:locations[0].zip..."); }); emp.get('works_for').on('change:controls[0]->locations[0]->zip', function () { ok(true, "Fired emp.works_for change:controls[0]->locations[0]->zip..."); }); emp.get('works_for').get("locations").at(0).set('zip', 94405); equal(emp.get('works_for->controls[0]->locations[0]->zip'), 94405); equal(emp.get('dependents[0]->fname'), emp.get('dependents').at(0).get('fname')); // Change separator to `.` Backbone.Associations.SEPARATOR = "."; equal(Backbone.Associations.SEPARATOR, ".", "Backbone.Associations.SEPERATOR should be `.`"); }); test("primitive attribute set operation", 2, function () { emp.set({'age':22}); equal(emp.get("age"), 22, "emp's should be 22 years"); loc1.set({'zip':'95502'}); ok(dept1.get('locations').at(0) == loc1, "dept1's first location should be same as loc1"); }); test("nested get", 11, function () { equal(emp.get('works_for.name'), emp.get('works_for').get('name'), 'result should be same as `get` chain'); equal(emp.get('dependents[0].fname'), emp.get('dependents').at(0).get('fname'), 'index can be defined to get model from collection, like `get("dependent[0].fname")`'); equal(emp.get('dependents[1].fname'), emp.get('dependents').at(1).get('fname')); equal(emp.get('works_for.controls[0].locations[0].zip'), 94404); equal(emp.get('works_for.controls[0].locations[0].zip'), emp.get('works_for').get('controls').at(0).get('locations').at(0).get('zip')); deepEqual(emp.get('works_for.locations[1]').toJSON(), emp.get('works_for').get('locations').at(1).toJSON()); equal(emp.get('dependents[1000].fname'), undefined, "result should be `undefined` if indexed model in not present in collection"); equal(emp.get('works_for.unknown'), undefined, "result should be `undefined` if attribute is not available"); equal(emp.get("1"), undefined); equal(emp.get('dependents[1]."1"'), undefined); equal(emp.get('works_for.""'), undefined); }); test("nested set", 6, function () { equal(emp.get('works_for.name'), 'R&D'); emp.set('works_for.name', 'Marketing'); equal(emp.get('works_for.name'), 'Marketing'); emp.set('works_for.locations[0].zip', 94403); equal(emp.get('works_for.locations[0].zip'), 94403, "nested `set` for model in collection should be same as normal `set`"); emp.set('dependents[0].sex', 'X', {validate:true});//validate test notEqual(emp.get('dependents[0].sex'), 'X', "validate test should be passed in nested `set` while `validate:true` is passed"); emp.set({ 'designation':'Senior Manager', 'works_for.controls[0].locations[0].zip':90909, 'dependents[1000].fname':'outofindex', 'dependents[1].fname':'John' }); equal(emp.get('dependents[1].fname'), 'John'); emp.on('change:works_for.name', function () { ok(false); }); emp.set({ 'works_for.name':'Marketing' }, {silent:true}); emp.set({ 'wrongpath.path2.works_for.name':'mip' }); ok(true); }); test("function can also be passed as value of attribute on set", 2, function () { var dept2 = function () { return { name:"Marketing", number:"24" }; }; equal(emp.get("works_for").get("name"), "R&D", "department name should be R&D"); emp.set({"works_for":dept2}); equal(emp.get("works_for").get("name"), "Marketing", "department name should be set to Marketing"); }); test("unset", 3, function () { emp.get('works_for').unset('locations'); equal(emp.get('works_for').get('locations'), void 0, "locations should be void"); emp.unset('works_for.locations'); equal(emp.get('works_for.locations'), void 0, "locations should be void"); emp.unset('dependents'); equal(emp.get('dependents'), void 0, "`dependents` should be unset"); }); test("setDefaults", 2, function () { emp.get("works_for").set({'number':5}); equal(emp.get("works_for").get('number'), 5, "number has new value"); emp.set({"works_for":dept1.defaults}); equal(emp.get("works_for").get('number'), -1, "number has default value"); }); test("invalid relations", 5, function () { var em1 = Backbone.AssociatedModel.extend({ relations:[ { type1:Backbone.One, //no type specified key:'parent', relatedModel:'Node' } ] }); try { var emi1 = new em1; emi1.set('parent', {id:1}); } catch (e) { equal(e.message === "type attribute must be specified and have the values Backbone.One or Backbone.Many", true) } var em2 = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.Oxne, //wrong value of type key:'parent', relatedModel:'Node' } ] }); try { var em2i = new em2; em2i.set('parent', {id:1}); } catch (e) { equal(e.message === "type attribute must be specified and have the values Backbone.One or Backbone.Many", true) } var em3 = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'parent', collectionType:em2//no RelatedModel specified } ] }); try { var em3i = new em3; em3i.set('parent', {id:1}); } catch (e) { equal(e.message === "specify a relatedModel for Backbone.One type", true) } var em4 = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.Many, key: 'parent', collectionType: em2//RelatedModel specified } ] }); try { var em4i = new em4; em4i.set('parent', {id: 1}); } catch (e) { equal(e.message === "type is of Backbone.Model. Specify derivatives of Backbone.Collection", true) } var Owner = Backbone.OriginalModel.extend(); var House = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'owner', relatedModel:Owner } ] }); var owner = new Owner; try { new House({owner:owner}); } catch (e) { equal(e.message === "specify an AssociatedModel or Backbone.Model for Backbone.One type", true) } }); test("escape", 1, function () { emp.get('works_for').get("locations").at(0).set({'add1':'<a>New Address</a>'}); equal(_.escape(emp.get('works_for').get("locations").at(0).get("add1")), '&lt;a&gt;New Address&lt;/a&gt;', "City should be in HTML-escaped version"); }); test("has", 5, function () { ok(emp.get("works_for").get("locations").at(0).has('add2') == false, "Add2 is undefined in department address"); strictEqual(emp.has("works_for.locations[0].add2"), false); emp.get("works_for").get("locations").at(0).set({'add2':'Add2 value'}); strictEqual(emp.has("works_for.locations[0].add2"), true); strictEqual(emp.get("works_for.locations").at(0).has('add2'), true, "Add2 is defined in department address"); strictEqual(emp.has('add2'), false, "Add2 is undefined in patient"); }); test("validate", 2, function () { emp.get('dependents').at(0).set({sex:"X"}, {validate:true}); equal(emp.get('dependents').at(0).get('sex'), 'F', "sex validation prevents values other than M & F"); emp.get('dependents').at(0).set({sex:"M"}, {validate:true}); equal(emp.get('dependents').at(0).get('sex'), 'M', "sex validation allows legal values - M & F"); }); test("clear", 2, function () { emp.clear(); equal(emp.get('works_for'), void 0, "Deparment should be set to undefined"); equal(emp.get('dependents'), void 0, "Dependents should be undefined"); }); test("clone", 7, function () { var emp2 = emp.clone(); equal(emp.get('fname'), 'John'); equal(emp.get('works_for').get('name'), 'R&D'); equal(emp2.get('fname'), emp.get('fname'), "fname should be the same on the clone."); equal(emp2.get('works_for').get('name'), emp.get('works_for').get('name'), "name of department should be the same on the clone."); deepEqual(emp.toJSON(), emp2.toJSON(), "emp should be the same on the clone"); emp.set({ works_for:{ name:'Marketing', number:'24' } }); equal(emp.get('works_for').get('name'), 'Marketing'); equal(emp2.get('works_for').get('name'), 'R&D', "Changing a parent attribute does not change the clone."); }); test("change, hasChanged, changedAttributes, previous, previousAttributes", 8, function () { emp.on('change', function () { ok(emp.hasChanged('works_for'), "emp->change, employee has changed"); }); emp.on('change:works_for', function () { equal(true, emp.hasChanged()); equal(true, emp.hasChanged('works_for')); equal(emp.get('works_for').hasChanged(), false, '`hasChanged` for `works_for` returns false as it is new object'); equal(emp.hasChanged('works_for'), true); equal(emp.get('works_for').changedAttributes(), false, 'changedAttributes for `works_for` returns false as it is new object'); equal(emp.previous('works_for').get('name'), 'R&D'); equal(emp.previousAttributes().works_for.get('name'), 'R&D', 'previousAttributes is correct'); }); emp.set('works_for', {name:'Marketing', number:'24'}); }); test("change : second model of nested collection", 1, function () { dept1.on('change:locations[0]', function () { ok(false, 'First model in nested collection should not change'); }); dept1.on('change:locations[1]', function () { ok(true, 'Second model in nested collection should change'); }); loc2.set('zip', '97008'); }); test("change : all attributes get updated in an atomic operation", 8, function () { emp.on('change', function () { equal(emp.get('works_for').get('name'), 'Marketing'); equal(emp.get('works_for').get('number'), '24'); equal(emp.previous('works_for').get('name'), 'R&D'); equal(emp.previous('works_for').get('number'), '23'); }); emp.on('change:works_for', function () { equal(emp.get('works_for').get('name'), 'Marketing'); equal(emp.get('works_for').get('number'), '24'); equal(emp.previous('works_for').get('name'), 'R&D'); equal(emp.previous('works_for').get('number'), '23'); }); emp.set('works_for', {name:'Marketing', number:'24'}); }); test("change : all attributes get updated in an atomic operation for AssociatedModel properties ", 14, function () { emp.on('change', function () { equal(emp.get('lname'), 'Bond'); equal(emp.get('fname'), 'James'); equal(emp.previous('fname'), 'John'); equal(emp.previous('lname'), 'Smith'); equal(emp.get('works_for').get('number'), '24'); equal(emp.previous('works_for').get('name'), 'R&D'); equal(emp.previous('works_for').get('number'), 23); }); emp.on('change:works_for', function () { equal(emp.get('lname'), 'Bond'); equal(emp.get('fname'), 'James'); equal(emp.previous('fname'), 'John'); equal(emp.previous('lname'), 'Smith'); equal(emp.get('works_for').get('number'), '24'); equal(emp.previous('works_for').get('name'), 'R&D'); equal(emp.previous('works_for').get('number'), 23); }); emp.set({works_for:{name:'Marketing', number:'24'}, fname:"James", lname:"Bond"}); }); test("child `change`", 17, function () { emp.on('change', function () { ok(true, "Fired emp change..."); }); emp.on('change:works_for', function () { ok(true, "Fired emp change:works_for..."); }); emp.on('change:works_for.name', function () { equal(true, emp.get("works_for").hasChanged()); equal(true, emp.hasChanged()); equal(true, emp.hasChanged("works_for")); var changed = emp.changedAttributes(); deepEqual(changed['works_for'].toJSON(), emp.get("works_for").toJSON()); equal(emp.get("works_for").previousAttributes()["name"], "R&D"); equal(emp.get("works_for").previous("name"), "R&D"); var diff = emp.get('works_for').toJSON(); diff.locations[0].zip = 94405; changed = emp.get('works_for').changedAttributes(diff); equal(changed.locations[0].zip, 94405); ok(true, "Fired emp change:works_for.name..."); }); emp.get('works_for').on('change', function () { ok(true, "Fired works_for change..."); }); emp.get('works_for').on('change:name', function () { ok(true, "Fired works_for dept:name change..."); }); emp.set({'works_for.name':'Marketing'});//4+7 emp.set('works_for', {name:"Marketing", number:29});//2 emp.set('works_for', undefined);//2 emp.set('works_for', dept1);//2 emp.set('works_for', dept1);//0 }); test("child `change in collection`", 18, function () { emp.get('works_for').get('locations').at(0).on('change:zip', function () { ok(true, "Fired works_for locations[0]:zip change..."); }); emp.get('works_for').get('locations').at(0).on('change', function () { equal(true, emp.get('works_for').hasChanged()); equal(true, emp.hasChanged()); var changed = emp.get('works_for').changedAttributes(); equal(changed['locations'].at(0).changed['zip'], 94403); equal(changed['controls'].at(0).changed['locations'].at(0).changed['zip'], 94403); ok(true, "Fired works_for locations0 change..."); }); emp.get('works_for').on('change:locations[*]', function () { ok(true, "Fired emp.works_for change:locations[*]..."); }); emp.get('works_for').on('change:locations[0].zip', function () { ok(true, "Fired emp.works_for change:locations[0].zip..."); }); emp.get('works_for').on('change:locations[0]', function () { ok(true, "Fired emp.works_for change:locations[0]..."); }); emp.get('works_for').on('change:controls[0].locations[0].zip', function () { ok(true, "Fired emp.works_for change:controls[0].locations[0].zip..."); }); emp.get('works_for').on('change:controls[0].locations[0]', function () { ok(true, "Fired emp.works_for change:controls[0].locations[0]..."); }); emp.get('works_for').on('change:controls.locations', function () { ok(true, "Fired emp.works_for change:controls.locations..."); }); emp.on('change:works_for.locations[0].zip', function () { ok(true, "Fired emp change:works_for.locations[0].zip..."); }); emp.on('change:works_for.locations[0]', function () { ok(true, "Fired emp change:works_for.locations[0]..."); }); emp.on('change:works_for.locations[*]', function () { ok(true, "Fired emp change:works_for.locations[*]..."); }); emp.on('change:works_for.controls[0].locations[0].zip', function () { ok(true, "Fired emp change:works_for.controls[0].locations[0].zip..."); }); emp.on('change:works_for.controls[0].locations[0]', function () { ok(true, "Fired emp change:works_for.controls[0].locations[0]..."); }); emp.on('change:works_for.controls[*].locations[*]', function () { ok(true, "Fired emp change:works_for.controls[*].locations[*]..."); }); emp.on('change:works_for.controls[*].locations[*].zip', function () { ok(true, "Fired emp change:works_for.controls[*].locations[*].zip..."); }); emp.get('works_for').get("locations").at(0).set('zip', 94403); }); test("collection `*` change", 12, function () { emp.on('change:works_for.controls[0].locations[0]', function () { ok(true, "Fired emp change:works_for.controls[0].locations[0]"); }); emp.on('change:works_for.controls[*].locations[*]', function () { ok(true, "Fired emp change:works_for.controls[*].locations[*]"); }); emp.on('change:works_for.controls[*].locations[*].zip', function () { ok(true, "Fired emp change:works_for.controls[*].locations[*].zip"); }); emp.on('change:works_for.controls[1].locations', function () { ok(true, "Fired emp change:works_for.controls[1].locations"); }); emp.on('change:works_for.controls[*].locations', function () { ok(true, "Fired emp change:works_for.controls[*].locations"); }); emp.get('works_for.controls[0].locations[0]').set('zip', 94406); //3 emp.get('works_for.controls[1].locations[0]').set('add1', 'new changed address'); //1 emp.set('works_for.controls[1].locations', undefined); // 2 emp.set('works_for.controls[1].locations', [loc1]); // 2 emp.on('change:works_for.locations[0]', function () { ok(false, "emp change:works_for.locations[0] should not be fired"); }); emp.on('change:works_for.locations[1]', function () { ok(true, "Fired emp change:works_for.locations[1]"); }); emp.on('change:works_for.locations[*]', function () { ok(true, "Fired emp change:works_for.locations[*]"); }); emp.get('works_for.locations[1]').set('zip', 94407); //2 emp.get('works_for').on('change:locations', function () { ok(true, "Fired emp.works_for change:locations..."); }); emp.set('works_for.locations', undefined); //1 emp.set('works_for.locations', [loc1]); //1 }); test("collection `*` add", 5, function () { emp.on('add:works_for.controls[0].locations', function () { ok(true, "Fired emp add:works_for.controls[0].locations..."); }); emp.on('add:works_for.controls[1].locations', function () { ok(true, "Fired emp add:works_for.controls[1].locations..."); }); emp.on('add:works_for.controls[*].locations', function () { ok(true, "Fired emp add:works_for.controls[*].locations..."); }); emp.on('add:works_for.locations', function () { ok(true, "add:works_for.locations"); }); emp.on('add:works_for.locations[*]', function () { ok(false, "emp add:works_for.location[*] should not be fired."); }); emp.get('works_for.controls[0].locations').add(loc2); //2 emp.get('works_for.controls[1].locations').add({ //2 id:3, add1:"loc3" }); emp.get('works_for.locations').add({ //1 id:4, add1:"loc4" }); }); test("collection `*` remove", 5, function () { emp.get('works_for.controls[0].locations').add(loc2); emp.get('works_for.locations').add(loc2); emp.on('remove:works_for.controls[0].locations', function () { ok(true, "Fired emp remove:works_for.controls[0].locations..."); }); emp.on('remove:works_for.controls[1].locations', function () { ok(true, "Fired emp remove:works_for.controls[1].locations..."); }); emp.on('remove:works_for.controls[*].locations', function () { ok(true, "Fired emp remove:works_for.controls[*].locations..."); }); emp.on('remove:works_for.locations', function () { ok(true, "Fired remove:works_for.locations"); }); emp.on('remove:works_for.locations[*]', function () { ok(false, "emp remove:works_for.location[*] should not be fired."); }); emp.get('works_for.controls[0].locations').remove(loc2); //2 emp.get('works_for.controls[1].locations').remove(loc2); //2 emp.get('works_for.locations').remove(loc2); //1 }); test("collection `*` reset", 3, function () { emp.on('reset:works_for.controls[0].locations', function () { ok(true, "Fired emp reset:works_for.controls[0].locations"); }); emp.on('reset:works_for.controls[*].locations', function () { ok(true, "Fired emp reset:works_for.controls[*].locations"); }); emp.on('reset:works_for.locations', function () { ok(true, "Fired reset:works_for.locations"); }); emp.get('works_for.controls[0].locations').reset(); emp.get('works_for.locations').reset(); }); test("collection `*` destroy", function () { emp.on("destroy:works_for.controls[0].locations", function () { ok(true, "Fired emp destroy:works_for.controls[0].locations"); }); emp.on("destroy:works_for.controls[*].locations", function () { ok(true, "Fired emp destroy:works_for.controls[*].locations"); }); var loc = emp.get('works_for.controls[0].locations[0]'); loc.sync = function (method, model, options) { return options.success.call(this, null, options); }; loc.destroy(); }); test("collection `*` sort", function () { Backbone.Associations.EVENTS_NC = true; emp.on('change:works_for.controls[0].locations', function () { ok(true, "Fired emp change:works_for.controls[0].locations"); }); emp.on('change:works_for.controls[*].locations', function () { ok(true, "Fired emp change:works_for.controls[*].locations"); }); emp.get('works_for').on('change:controls[0]', function () { ok(true, "Fired emp change:works_for.controls[0]"); }); emp.on('nested-change', function () { ok(true, "Fired emp nested-change"); }); emp.set('works_for.controls[0].locations', locations); //4 // check length and add comparator var locCol = emp.get('works_for.controls[0].locations'); equal(locCol.length, 6, "location collection's length should be 6."); locCol.comparator = function (l) { return l.get("state"); }; emp.on('sort:works_for.controls[0].locations', function () { ok(true, "Fired emp sort:works_for.controls[0].locations"); }); emp.on('sort:works_for.controls[*].locations', function () { ok(true, "Fired emp sort:works_for.controls[*].locations"); }); emp.on('sort:works_for.controls.locations', function () { ok(false, "emp sort:works_for.controls.locations should not be fired"); }); emp.get('works_for.controls[0].locations').sort(); }); test("child `nested-change`", 9, function () { Backbone.Associations.EVENTS_NC = true; emp.get('works_for').get('locations').on('change', function () { ok(true, "Regular backbone change event from collections..."); }); emp.get('works_for').on('nested-change', function () { if (arguments[0] == "controls[0].locations[0]") ok(true, "Fired emp.works_for change:controls[0].locations[0]..."); if (arguments[0] == "locations[0]") ok(true, "Fired emp.works_for change:locations[0]..."); equal(arguments[2].dummy, true); }); emp.on('nested-change', function () { if (arguments[0] == "works_for.controls[0].locations[0]") ok(true, "Fired emp change:works_for.controls[0].locations[0]..."); if (arguments[0] == "works_for.locations[0]") ok(true, "Fired emp change:works_for.locations[0]..."); equal(arguments[2].dummy, true); }); emp.get('works_for').get("locations").at(0).set('zip', 94403, {dummy:true}); }); test("Set closure scope correctly - while setting BB Collection & Model instances directly", 5, function () { var Location = Backbone.AssociatedModel.extend({ defaults:{ name:"", zip:"" } }); var Group = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'meetup', relatedModel:Location } ], defaults:{ meetup:undefined, name:"" } }); var loc1 = new Location({name:"CA", zip:"94404", id:1}); var loc2 = new Location({name:"OH", zip:"92201", id:2}); var cb = function () { ok(true) }; var g1 = new Group({name:"AO", meetup:loc1}); g1.on("change:meetup", cb); var g2 = new Group({name:"SO", meetup:loc2}); g2.on("change:meetup", cb); g1.set('meetup', loc2); //1 g2.set('meetup.zip', '93303'); //2 : Should fire change for both g1 and g2 g1.set('meetup', undefined); //1 g2.set('meetup.zip', '93304'); //1 : Should fire change for g2 only }); test("Fetch collection repeatedly: Issue#47", 3, function () { var User = Backbone.AssociatedModel.extend({ defaults:{ name:"", email:"" } }); var Container = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.Many, key:'xxx', relatedModel:User } ], defaults:{ 'xxx':[], name:"" } }); var ContainerCollection = Backbone.Collection.extend({ model:Container, url:"fc", sync:function (resp, model, options) { options.success( [ { "id":425, "name":"I am so cool", "xxx":[ { "id":418, "email":"art@garfunkel.com" } ] } ] ); } }); var f = new ContainerCollection(); f.on('add:xxx', function () { ok(true) }); f.fetch(); f.at(0).get('xxx').add({name:"n1"}); //Fire add:xxx f.at(0).get('xxx').add({name:"n4"}); //Fire add:xxx f.fetch(); f.at(0).get('xxx').add({name:"n2"}); //Fire add:xxx }); test("Polymorphic Associate : Issue#23", 4, function () { var Models = {}; var findPolyMorphicType = Models.findPolyMorphicType = function (relation, attributes) { var key = relation.key + '_type'; return Models[attributes[key] || this.get(key)]; }; Models.Job = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'organizable', relatedModel:findPolyMorphicType } ] }); Models.Comment = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'commentable', relatedModel:findPolyMorphicType } ] }); Models.Company = Backbone.AssociatedModel.extend({ }); Models.Department = Backbone.AssociatedModel.extend({ }); Models.Article = Backbone.AssociatedModel.extend({ }); Models.StatusUpdate = Backbone.AssociatedModel.extend({ }); var cjob = new Models.Job({organizable_type:'Company', name:"J1", organizable:{name:"Google"}}); var djob = new Models.Job({organizable_type:'Department', name:"J2", organizable:{name:"Google Reader"}}); var articleComment = new Models.Comment({ commentable_type:'Article', name:"c1", commentable:{body:"Wonderful post!"} }); var statusComment = new Models.Comment({ commentable_type:'StatusUpdate', name:"c2", commentable:{body:"Why are you updating your status with pointless crap?"} }); equal(cjob.get('organizable') instanceof Models.Company, true); equal(djob.get('organizable') instanceof Models.Department, true); equal(articleComment.get('commentable') instanceof Models.Article, true); equal(statusComment.get('commentable') instanceof Models.StatusUpdate, true); }); test("Fetch collection with reset: Issue#45", 5, function () { var Product = Backbone.AssociatedModel.extend({ defaults:{ name:undefined, // String productId:undefined // String } }); var Category = Backbone.AssociatedModel.extend(); var Brand = Backbone.AssociatedModel.extend(); var Vendor = Backbone.AssociatedModel.extend(); var Tag = Backbone.AssociatedModel.extend(); var SearchFacet = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.Many, key:'categories', relatedModel:Category }, { type:Backbone.Many, key:'brands', relatedModel:Brand }, { type:Backbone.Many, key:'vendors', relatedModel:Vendor }, { type:Backbone.Many, key:'tags', relatedModel:Tag } ] }); var counter = 1; var SearchResult = Backbone.AssociatedModel.extend({ relations:[ { type:Backbone.One, key:'searchFacet', relatedModel:SearchFacet }, { type:Backbone.Many, key:'products', relatedModel:Product } ], sync:function (method, model, options) { return options.success.call(this, { id:1, products:[ { id:counter, name:'product' + counter, productId:'productId' + counter++ }, { id:counter, name:'product' + counter, productId:'productId' + counter++ }, { id:counter, name:'product' + counter, productId:'productId' + counter++ }, { id:counter, name:'product' + counter, productId:'productId' + counter++ } ], searchFacet:{ id:'sf1', categories:[ { name:'category' + counter++ } ], brands:[ { name:'brand' + counter++ } ], tags:[ { name:'tag' + counter++ } ], vendors:[ { name:'vendor' + counter++ } ] } }); }, urlRoot:'/search' }); var searchResult = new SearchResult(); searchResult.on("reset:products", function (e) { ok(false, 'searchResult reset:products should not fired.'); //0 }); searchResult.on("reset:searchFacet.tags", function (e) { ok(false, 'searchResult reset:searchFacet.tags should not fired.'); //0 }); searchResult.fetch(); equal(searchResult.get('products').length, 4, "searchResult.products.length should be 4."); //1 searchResult.fetch(); equal(searchResult.get('products').length, 4, "searchResult.products.length should be 4."); //1 searchResult.off(); searchResult.on("reset:products", function (e) { ok(true, 'searchResult reset:products fired.'); //1 }); searchResult.on("reset:searchFacet.tags", function (e) { ok(true, 'searchResult reset:searchFacet.tags fired.'); //1 }); searchResult.fetch({reset:true}); equal(searchResult.get('products').length, 4, "searchResult.products.length should be 4."); //1 }); test("IdAttribute: Issue#80", 2, function () { var User = Backbone.AssociatedModel.extend({ idAttribute: "_id" }); var Container = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.One, key: 'owner', relatedModel: User } ], defaults: { users: [] } }); var home = new Container(); var u = new User({_id: 1, name: "Chip Lay"}); home.set('owner', u); var u1 = new User({_id: 1, name: "Chip Lay"}); home.set('owner', u1); equal(home.get('owner') == u, true); //Uses the same id to refer the same object User = Backbone.AssociatedModel.extend(); Container = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.One, key: 'owner', relatedModel: User } ], defaults: { users: [] } }); home = new Container(); u = new User({id: 1, name: "Chip Lay"}); home.set('owner', u); u1 = new User({id: 1, name: "Chip Lay"}); home.set('owner', u1); equal(home.get('owner') == u, true); //Uses the same id to refer the same object }); test("Global scope support: Issue#98", 3, function () { var scope1 = {}; var scope2 = {}; Backbone.Associations.scopes.push(scope1); Backbone.Associations.scopes.push(scope2); scope1.User = Backbone.AssociatedModel.extend({ defaults: {omid: "s1.u"} }); scope2.User = Backbone.AssociatedModel.extend({ defaults: {omid: "s2.u"} }); var Container = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.One, key: 'owner', relatedModel: 'User' } ], defaults: { users: [] } }); var home = new Container(); home.set('owner', {id: 1, name: "Chip Lay"}); equal(home.get('owner').get('omid') == 's1.u', true); Backbone.Associations.scopes = []; Backbone.Associations.scopes.push(scope2); Backbone.Associations.scopes.push(scope1); home.set('owner', {id: 2, name: "Chip Lay"}); equal(home.get('owner').get('omid') == 's2.u', true); //scope1 takes precedence over scope 2 home.relations[0].scope = scope1; home.set('owner', {id: 3, name: "Chip Lay"}); equal(home.get('owner').get('omid') == 's1.u', true); }); test("Remote Key: Issue#78", 11, function () { var Location = Backbone.AssociatedModel.extend({}); var Job = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.One, key: 'location', isTransient: true, relatedModel: Location } ] }); var User = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.One, key: 'job', remoteKey: 'job_attributes', relatedModel: Job } ] }); var MultiContainer = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.Many, key: 'owner', remoteKey : 'owner_attributes', relatedModel: User } ] }); var home = new MultiContainer(); var u = new User({name: "Chip Lay", job: new Job({name: 'dream_job', location: new Location({name: 'loc1'})})}); var u1 = new User({name: "John Sr"}); home.set('owner', [u,u1]); equal(home.toJSON()['owner_attributes'].length,2); equal(home.toJSON()['owner'],undefined); var UniContainer = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.One, key: 'owner', remoteKey : 'owner_attributes', relatedModel: User } ] }); var home2 = new UniContainer(); home2.set('owner', u); equal(home2.toJSON()['owner_attributes']['name'],'Chip Lay'); equal(home2.toJSON()['owner_attributes']['job'], undefined); equal(home2.toJSON()['owner_attributes']['job_attributes']['name'], 'dream_job'); equal(home2.toJSON()['owner_attributes']['job_attributes']['location'], undefined); equal(home2.toJSON()['owner'],undefined); var home3 = new UniContainer(); equal(home3.toJSON()['owner_attributes'],undefined); equal(home3.toJSON()['owner'],undefined); var home4 = new MultiContainer(); equal(home4.toJSON()['owner_attri