Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion backbone-relational.js
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,8 @@

this.listenTo( collection, 'relational:add', this.handleAddition )
.listenTo( collection, 'relational:remove', this.handleRemoval )
.listenTo( collection, 'relational:reset', this.handleReset );
.listenTo( collection, 'relational:reset', this.handleReset )
.listenTo( collection, 'relational:update', this.handleUpdate );

return collection;
},
Expand Down Expand Up @@ -1139,6 +1140,16 @@
});
},

handleUpdate: function ( coll, options ) {
// If this change is not inside a set (an onChanged handler), ensure that
// change events for the key and the object are queued.
var dit = this;
!options.silent && !dit.instance._relationalModelChangingViaSet && Backbone.Relational.eventQueue.add( function() {
dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
dit.instance.trigger( 'change', dit.instance, options, null, true );
});
},

tryAddRelated: function( model, coll, options ) {
var item = _.contains( this.keyIds, model.id );

Expand Down Expand Up @@ -1505,6 +1516,7 @@
},

set: function( key, value, options ) {
this._relationalModelChangingViaSet = true;
Backbone.Relational.eventQueue.block();

// Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
Expand Down Expand Up @@ -1554,9 +1566,30 @@
Backbone.Relational.eventQueue.unblock();
}

this._relationalModelChangingViaSet = false;
return result;
},

fetch: function( options ) {
var dit = this;
var args = arguments;
var promise = Backbone.Model.prototype.fetch.apply( dit, arguments );
if ( options && options.fetchRelationships && !_.isEmpty( dit._relations ) ) {
promise = promise.then( function( value ) {
var relationshipPromises = _.map( dit._relations, function( rel ) {
return dit.getAsync( rel.key, _.omit( options, 'fetchRelationships' ) );
});
relationshipPromises.splice(0, 0, new Promise( function( resolve, reject ) {
resolve( value );
}));
return Promise.all( relationshipPromises );
}).then( function( values ) {
return values[0];
});
}
return promise;
},

clone: function() {
var attributes = _.clone( this.attributes );
if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) {
Expand Down Expand Up @@ -1973,6 +2006,10 @@
}
}

if ( !_.isEmpty( toAdd ) ) {
this.trigger('relational:update', this, options)
}

return result;
};

Expand Down Expand Up @@ -2000,6 +2037,10 @@
this.trigger( 'relational:remove', model, this, options );
}, this );

if ( !_.isEmpty( toRemove ) ) {
this.trigger('relational:update', this, options);
}

return result;
};

Expand Down
102 changes: 97 additions & 5 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,40 @@ $(document).ready(function() {
_.last( window.requests ).respond( 200, { id: 'f-2', name: 'Cheese' } );
});

test( "fetchRelationships recursively fetches relationships", function() {
var personCounter = 0;
var Person = Backbone.RelationalModel.extend({
urlRoot: '/people/',
// This is ultimately what the server would return when asked for a
// single person model, but there's no way to specify nested responses
// in the test framework. The value is just faked here with stateful
// parsing.
parse: function (resp, options) {
personCounter++;
return {id: "person"+personCounter};
}
});
var People = Backbone.Collection.extend({
model: Person,
url: '/people/'
});
var Family = Backbone.RelationalModel.extend({
urlRoot: '/families/',
relations: [
{
type: Backbone.HasMany,
key: 'members',
collectionType: People,
relatedModel: Person
}
],
});

var family = new Family({id: 'family1'});
family.fetch( {fetchRelationships: true, response: {status: 200, responseText: {id: "family1", members: ["person1", "person2"]}}} );
equal( window.requests.length, 3, "fetched family1, person1, and person2" );
});

test( "autoFetch a HasMany relation", function() {
var shopOne = new Shop({
id: 'shop-1',
Expand Down Expand Up @@ -1639,6 +1673,61 @@ $(document).ready(function() {
equal( changedAttrs.color, 'red', '... with correct properties in "changedAttributes"' );
});

test( 'collection updates should fire change events on parent objects', function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );

scope.PetAnimal = Backbone.RelationalModel.extend({
subModelTypes: {
'cat': 'Cat',
'dog': 'Dog'
}
});
scope.Dog = scope.PetAnimal.extend();
scope.Cat = scope.PetAnimal.extend();

scope.PetOwner = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'pets',
relatedModel: scope.PetAnimal,
reverseRelation: {
key: 'owner'
}
}]
});

var owner = new scope.PetOwner( { id: 'owner-2354' } );
var animal = new scope.Dog( { type: 'dog', id: '238902', color: 'blue' } );
owner.set(owner.parse({
id: 'owner-2354',
pets: [ animal ]
}));

var pets = owner.get('pets');
equal( pets.size(), 1, 'pets starts with one animal' );

var changes = 0;
owner.on('change change:pets', function () {
changes++;
});
pets.add( { id: '456780', type: 'dog', color: 'yellow' } );
equal( pets.size(), 2, 'added one animal to pets' );
equal( changes, 2, 'change events get called on owner for pets collection change' );

pets.remove( '456780' );
equal( pets.size(), 1, 'removed one animal from pets' );
equal( changes, 4, 'change events get called on owner for pets collection change' );

var animal2 = new scope.Dog( { id: '34567', type: 'dog', color: 'black' } );
owner.set(owner.parse({
id: 'owner-2354',
pets: [ animal, animal2 ]
}));
equal( pets.size(), 2, 'new array of pets is of size 2' );
equal( changes, 6, 'change events get called on owner for pets collection set via owner.set' );
});

test( 'change events should not fire on new items in Collection#set', function() {
var modelChangeEvents = 0,
collectionChangeEvents = 0;
Expand Down Expand Up @@ -4657,8 +4746,8 @@ $(document).ready(function() {
change = changeAnimals = animalChange = 0;
animals.add( 'a2' );

ok( change === 0, 'change event not should fire' );
ok( changeAnimals === 0, 'no change:animals event should fire' );
ok( change === 1, 'change event should fire' );
ok( changeAnimals === 1, 'change:animals event should fire' );
ok( animalChange === 0, 'no animals:change event should fire' );

// Update an animal directly
Expand All @@ -4673,8 +4762,8 @@ $(document).ready(function() {
change = changeAnimals = animalChange = 0;
animals.remove( 'a2' );

ok( change === 0, 'no change event should fire' );
ok( changeAnimals === 0, 'no change:animals event should fire' );
ok( change === 1, 'change event should fire' );
ok( changeAnimals === 1, 'change:animals event should fire' );
ok( animalChange === 0, 'no animals:change event should fire' );
});

Expand Down Expand Up @@ -4772,16 +4861,19 @@ $(document).ready(function() {
person
.on('change:livesIn', function() {
//console.log( arguments );

// Will cause a second change notification in `house`
house.set({livesIn: house});
})
.on( 'change', function () {
//console.log( arguments );
changeEventsTriggered++;
});

// Will cause a change notification in `house`
person.set({livesIn: house});

ok( changeEventsTriggered === 2, 'one change each triggered for `house` and `person`' );
ok( changeEventsTriggered === 3, 'one change triggered for `person`, two for `house`' );
});

module( "Performance", { setup: reset } );
Expand Down