Skip to content

Commit

Permalink
Add a parse option to relations so nested models can be parsed.
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulUithol committed Mar 5, 2013
1 parent 41d1aa4 commit 195b860
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 95 deletions.
42 changes: 29 additions & 13 deletions backbone-relational.js
Expand Up @@ -661,20 +661,24 @@
initialize: function( opts ) {
this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );

this.findRelated( opts );
var related = this.findRelated( opts );
this.setRelated( related );

// Notify new 'related' object of the new relation.
_.each( this.getReverseRelations(), function( relation ) {
relation.addRelated( this.instance, opts );
}, this );
},

/**
* Find related Models.
* @param {Object} [options]
* @return {Backbone.Model}
*/
findRelated: function( options ) {
var related = null;

/*if ( _.isObject( options ) && options.parse && !this.options.parse ) {
options.parse = false;
}*/
options = _.defaults( { parse: this.options.parse }, options );

if ( this.keyContents instanceof this.relatedModel ) {
related = this.keyContents;
Expand All @@ -684,7 +688,7 @@
related = this.relatedModel.findOrCreate( this.keyContents, opts );
}

this.setRelated( related );
return related;
},

/**
Expand All @@ -697,7 +701,8 @@
},

/**
* If the key is changed, notify old & new reverse relations and initialize the new relation
* Event handler for `change:<key>`.
* If the key is changed, notify old & new reverse relations and initialize the new relation.
*/
onChange: function( model, attr, options ) {
// Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
Expand All @@ -715,7 +720,8 @@

if ( changed ) {
this.setKeyContents( attr );
this.findRelated( options );
var related = this.findRelated( options );
this.setRelated( related );
}

// Notify old 'related' object of the terminated relation
Expand Down Expand Up @@ -802,7 +808,8 @@
throw new Error( '`collectionType` must inherit from Backbone.Collection' );
}

this.findRelated( opts );
var related = this.findRelated( opts );
this.setRelated( related );
},

/**
Expand Down Expand Up @@ -845,9 +852,16 @@
return collection;
},

/**
* Find related Models.
* @param {Object} [options]
* @return {Backbone.Collection}
*/
findRelated: function( options ) {
var related = null;

options = _.defaults( { parse: this.options.parse }, options );

// Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection
if ( this.keyContents instanceof Backbone.Collection ) {
this._prepareCollection( this.keyContents );
Expand Down Expand Up @@ -877,10 +891,10 @@
related = this._prepareCollection();
}

related.update( toAdd, _.defaults( { merge: false }, options ) );
related.update( toAdd, _.defaults( { merge: false, parse: false }, options ) );
}

this.setRelated( related );
return related;
},

/**
Expand All @@ -905,14 +919,16 @@
},

/**
* If the key is changed, notify old & new reverse relations and initialize the new relation
* Event handler for `change:<key>`.
* If the contents of the key are changed, notify old & new reverse relations and initialize the new relation.
*/
onChange: function( model, attr, options ) {
options = options ? _.clone( options ) : {};
this.setKeyContents( attr );
this.changed = false;

this.findRelated( options );
var related = this.findRelated( options );
this.setRelated( related );

if ( !options.silent ) {
var dit = this;
Expand All @@ -929,7 +945,7 @@
/**
* When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
* (should be 'HasOne', must set 'this.instance' as their related).
*/
*/
handleAddition: function( model, coll, options ) {
//console.debug('handleAddition called; args=%o', arguments);
options = options ? _.clone( options ) : {};
Expand Down
181 changes: 99 additions & 82 deletions test/tests.js
Expand Up @@ -1011,8 +1011,7 @@ $(document).ready(function() {

// `parse` gets called once by `findOrCreate` directly when trying to lookup `1`,
// once when `build` (called from `findOrCreate`) calls the Zoo constructor with `{ parse: true}`.
// Same for the nested `Animal`.
ok( parseCalled === 4, 'parse called 4 times? ' + parseCalled );
ok( parseCalled === 2, 'parse called 2 times? ' + parseCalled );

parseCalled = 0;

Expand All @@ -1029,94 +1028,22 @@ $(document).ready(function() {
ok( animal.get( 'livesIn' ) instanceof Zoo );
ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );

// `parse` gets called once by `findOrCreate` directly when trying to lookup `b`,
// and once when `build` (called from `findOrCreate`) calls its constructor
ok( parseCalled === 2, 'parse called 2 times? ' + parseCalled );
ok( parseCalled === 0, 'parse called 0 times? ' + parseCalled );

// Reset `parse` methods
Zoo.prototype.parse = Animal.prototype.parse = Backbone.RelationalModel.prototype.parse;
});

test( "`parse` with a nested collection", function() {
var parseCalled = 0;
Zoo.prototype.parse = Animal.prototype.parse = function( resp, options ) {
parseCalled++;
return resp.model;
};

var zoo = new Zoo({
model: {
id: 'z1',
name: 'San Diego Zoo',
animals: [ { model: { id: 'a1' } } ]
}
}, { parse: true } );
var animal = zoo.get( 'animals' ).first();

ok( animal instanceof Animal );
ok( animal.id === 'a1' );
ok( animal.get( 'livesIn' ) instanceof Zoo );

// `parse` called by `Zoo` constructor, `Animal.findOrCreate`, `Animal` constructor
ok( parseCalled === 3, 'parse called 3 times? ' + parseCalled );

// Reset `parse` methods
Zoo.prototype.parse = Animal.prototype.parse = Backbone.RelationalModel.prototype.parse;
});

test( "`parse` with deeply nested relations", function() {
var parseCalled = 0;
Company.prototype.parse = Job.prototype.parse = Person.prototype.parse = function( resp, options ) {
parseCalled++;
var data = _.clone( resp.model );
data.id = data.id.uri;
return data;
};

var data = {
model: {
id: { uri: 'c1' },
employees: [
{
model: {
id: { uri: 'e1' },
person: { model: { id: { uri: 'p1' } } }
}
},
{
model: {
id: { uri: 'e2' },
person: { model: { id: { uri: 'p2' } } }
}
}
]
}
};

var company = new Company( data, { parse: true } ),
employees = company.get( 'employees' ),
job = employees.first(),
person = job.get( 'person' );

ok( job && job.id === 'e1', 'job exists' );
ok( person && person.id === 'p1', 'person exists' );

ok( parseCalled === 9, 'parse called 9 times? ' + parseCalled );

// Reset `parse` methods
Company.prototype.parse = Job.prototype.parse = Person.prototype.parse = Backbone.RelationalModel.prototype.parse;
});

test( "`parse` should only get called on top-level objects; not for nested models and collections", function() {
test( "By default, `parse` should only get called on top-level objects; not for nested models and collections", function() {
var companyData = {
'data': {
'id': 'company-1',
'contacts': [
{
'id': '1013855'
'id': '1'
},
{
'id': '1949517'
'id': '2'
}
]
}
Expand Down Expand Up @@ -1147,16 +1074,15 @@ $(document).ready(function() {
var parseCalled = 0;
Company.prototype.parse = Contact.prototype.parse = Contacts.prototype.parse = function( resp, options ) {
parseCalled++;
options && ( options.parse = false );
return resp.data;
return resp.data || resp;
};

var company = new Company( companyData, { parse: true } ),
contacts = company.get( 'contacts' ),
contact = contacts.first();

ok( company.id === 'company-1' );
ok( contact && contact.id === '1013855', 'contact exists' );
ok( contact && contact.id === '1', 'contact exists' );
ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );

// simulate what would happen if company.fetch() was called.
Expand All @@ -1168,7 +1094,7 @@ $(document).ready(function() {

ok( contacts === company.get( 'contacts' ), 'contacts collection is same instance after fetch' );
equal( contacts.length, 2, '... with correct length' );
ok( contact && contact.id === '1013855', 'contact exists' );
ok( contact && contact.id === '1', 'contact exists' );
ok( contact === contacts.first(), '... and same model instances' );
});

Expand Down Expand Up @@ -1603,6 +1529,97 @@ $(document).ready(function() {
var zoo = new Zoo();
ok( zoo.get("animals").url === "zoo/" + zoo.cid + "/animal/");
});

test( "`parse` with deeply nested relations", function() {
var collParseCalled = 0,
modelParseCalled = 0;

var Job = Backbone.RelationalModel.extend({});

var JobCollection = Backbone.Collection.extend({
model: Job,

parse: function( resp, options ) {
collParseCalled++;
return resp.data || resp;
}
});

var Company = Backbone.RelationalModel.extend({
relations: [{
type: 'HasMany',
key: 'employees',
parse: true,
relatedModel: Job,
collectionType: JobCollection,
reverseRelation: {
key: 'company'
}
}]
});

var Person = Backbone.RelationalModel.extend({
relations: [{
type: 'HasMany',
key: 'jobs',
parse: true,
relatedModel: Job,
collectionType: JobCollection,
reverseRelation: {
key: 'person',
parse: false
}
}],

parse: function( resp, options ) {
modelParseCalled++;
return resp;
}
});

Company.prototype.parse = Job.prototype.parse = function( resp, options ) {
modelParseCalled++;
var data = _.clone( resp.model );
data.id = data.id.uri;
return data;
};

var data = {
model: {
id: { uri: 'c1' },
employees: [
{
model: {
id: { uri: 'e1' },
person: {
id: 'p1',
jobs: [ 'e1', { model: { id: { uri: 'e3' } } } ]
}
}
},
{
model: {
id: { uri: 'e2' },
person: {
id: 'p2'
}
}
}
]
}
};

var company = new Company( data, { parse: true } ),
employees = company.get( 'employees' ),
job = employees.first(),
person = job.get( 'person' );

ok( job && job.id === 'e1', 'job exists' );
ok( person && person.id === 'p1', 'person exists' );

ok( modelParseCalled === 7, 'model.parse called 7 times? ' + modelParseCalled );
ok( collParseCalled === 0, 'coll.parse called 0 times? ' + collParseCalled );
});


module( "Backbone.Relation preconditions", { setup: reset } );
Expand Down

0 comments on commit 195b860

Please sign in to comment.