Skip to content

Commit

Permalink
Merge pull request #362 from kartofflly/deep-submodeltype-hierarchies
Browse files Browse the repository at this point in the history
Fixes #360. Adds support for deep subModelType hierarchies.
  • Loading branch information
PaulUithol committed Jul 23, 2013
2 parents 7e47f1c + 59b53bf commit bf6a2b5
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 38 deletions.
95 changes: 65 additions & 30 deletions backbone-relational.js
Original file line number Diff line number Diff line change
Expand Up @@ -1596,57 +1596,92 @@
* @return {Backbone.Model}
*/
build: function( attributes, options ) {
var model = this;

// 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
this.initializeModelHierarchy();

// Determine what type of (sub)model should be built if applicable.
// Lookup the proper subModelType in 'this._subModels'.
if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) {
var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ];
var subModelType = this._subModels[ subModelTypeAttribute ];
var model = this._findSubModelType(this, attributes) || this;

return new model( attributes, options );
},

/**
* Determines what type of (sub)model should be built if applicable.
* Looks up the proper subModelType in 'this._subModels', recursing into
* types until a match is found. Returns the applicable 'Backbone.Model'
* or null if no match is found.
* @param {Backbone.Model} type
* @param {Object} attributes
* @return {Backbone.Model}
*/
_findSubModelType: function (type, attributes) {
if ( type._subModels && type.prototype.subModelTypeAttribute in attributes ) {
var subModelTypeAttribute = attributes[type.prototype.subModelTypeAttribute];
var subModelType = type._subModels[subModelTypeAttribute];
if ( subModelType ) {
model = subModelType;
return subModelType;
} else {
// Recurse into subModelTypes to find a match
for ( subModelTypeAttribute in type._subModels ) {
subModelType = this._findSubModelType(type._subModels[subModelTypeAttribute], attributes);
if ( subModelType ) {
return subModelType;
}
}
}
}

return new model( attributes, options );
return null;
},

/**
*
*/
initializeModelHierarchy: function() {
// If we're here for the first time, try to determine if this modelType has a 'superModel'.
if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
Backbone.Relational.store.setupSuperModel( this );

// If a superModel has been found, copy relations from the _superModel if they haven't been
// inherited automatically (due to a redefinition of 'relations').
// Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail
// the isUndefined/isNull check next time.
if ( this._superModel && this._superModel.prototype.relations ) {
// Find relations that exist on the `_superModel`, but not yet on this model.
// Inherit any relations that have been defined in the parent model.
this.inheritRelations();

// If we came here through 'build' for a model that has 'subModelTypes' then try to initialize the ones that
// haven't been resolved yet.
if ( this.prototype.subModelTypes ) {
var resolvedSubModels = _.keys(this._subModels);
var unresolvedSubModels = _.omit(this.prototype.subModelTypes, resolvedSubModels);
_.each( unresolvedSubModels, function( subModelTypeName ) {
var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
subModelType && subModelType.initializeModelHierarchy();
});
}
},

inheritRelations: function() {
// Bail out if we've been here before.
if (!_.isUndefined( this._superModel ) && !_.isNull( this._superModel )) {
return;
}
// Try to initialize the _superModel.
Backbone.Relational.store.setupSuperModel( this );

// If a superModel has been found, copy relations from the _superModel if they haven't been inherited automatically
// (due to a redefinition of 'relations').
if ( this._superModel ) {
// The _superModel needs a chance to initialize its own inherited relations before we attempt to inherit relations
// from the _superModel. You don't want to call 'initializeModelHierarchy' because that could cause sub-models of
// this class to inherit their relations before this class has had chance to inherit it's relations.
this._superModel.inheritRelations();
if ( this._superModel.prototype.relations ) {
// Find relations that exist on the '_superModel', but not yet on this model.
var inheritedRelations = _.select( this._superModel.prototype.relations || [], function( superRel ) {
return !_.any( this.prototype.relations || [], function( rel ) {
return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key;
}, this );
}, this );

this.prototype.relations = inheritedRelations.concat( this.prototype.relations );
}
else {
this._superModel = false;
}
}

// If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each.
if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) {
_.each( this.prototype.subModelTypes || [], function( subModelTypeName ) {
var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
subModelType && subModelType.initializeModelHierarchy();
});
// Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail the
// isUndefined/isNull check next time.
else {
this._superModel = false;
}
},

Expand Down
173 changes: 165 additions & 8 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1405,7 +1405,12 @@ $(document).ready(function() {
'carnivore': 'Carnivore'
}
});
scope.Primate = scope.Mammal.extend();
scope.Primate = scope.Mammal.extend({
subModelTypes: {
'human': 'Human'
}
});
scope.Human = scope.Primate.extend();
scope.Carnivore = scope.Mammal.extend();

var MammalCollection = AnimalCollection.extend({
Expand All @@ -1414,11 +1419,13 @@ $(document).ready(function() {

var mammals = new MammalCollection( [
{ id: 5, species: 'chimp', type: 'primate' },
{ id: 6, species: 'panther', type: 'carnivore' }
{ id: 6, species: 'panther', type: 'carnivore' },
{ id: 7, species: 'person', type: 'human' }
]);

ok( mammals.at( 0 ) instanceof scope.Primate );
ok( mammals.at( 1 ) instanceof scope.Carnivore );
ok( mammals.at( 2 ) instanceof scope.Human );
});

test( "Object building based on type, when used in relations" , function() {
Expand All @@ -1431,8 +1438,13 @@ $(document).ready(function() {
'dog': 'Dog'
}
});
var Dog = scope.Dog = PetAnimal.extend();
var Dog = scope.Dog = PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
}
});
var Cat = scope.Cat = PetAnimal.extend();
var Poodle = scope.Poodle = Dog.extend();

var PetPerson = scope.PetPerson = Backbone.RelationalModel.extend({
relations: [{
Expand All @@ -1454,21 +1466,30 @@ $(document).ready(function() {
{
type: 'cat',
name: 'Whiskers'
},
{
type: 'poodle',
name: 'Mitsy'
}
]
});

ok( petPerson.get( 'pets' ).at( 0 ) instanceof Dog );
ok( petPerson.get( 'pets' ).at( 1 ) instanceof Cat );
ok( petPerson.get( 'pets' ).at( 2 ) instanceof Poodle );

petPerson.get( 'pets' ).add({
petPerson.get( 'pets' ).add([{
type: 'dog',
name: 'Spot II'
});
},{
type: 'poodle',
name: 'Mitsy II'
}]);

ok( petPerson.get( 'pets' ).at( 2 ) instanceof Dog );
ok( petPerson.get( 'pets' ).at( 3 ) instanceof Dog );
ok( petPerson.get( 'pets' ).at( 4 ) instanceof Poodle );
});

test( "Automatic sharing of 'superModel' relations" , function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );
Expand All @@ -1493,6 +1514,10 @@ $(document).ready(function() {
scope.Flea = Backbone.RelationalModel.extend({});

scope.Dog = scope.PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
},

relations: [{
type: Backbone.HasMany,
key: 'fleas',
Expand All @@ -1502,22 +1527,154 @@ $(document).ready(function() {
}
}]
});
scope.Poodle = scope.Dog.extend();

var dog = new scope.Dog({
name: 'Spot'
});

var poodle = new scope.Poodle({
name: 'Mitsy'
});

var person = new scope.PetPerson({
pets: [ dog ]
pets: [ dog, poodle ]
});

ok( dog.get( 'owner' ) === person, "Dog has a working owner relation." );
ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );

var flea = new scope.Flea({
host: dog
});

var flea2 = new scope.Flea({
host: poodle
});

ok( dog.get( 'fleas' ).at( 0 ) === flea, "Dog has a working fleas relation." );
ok( poodle.get( 'fleas' ).at( 0 ) === flea2, "Poodle has a working fleas relation." );
});

test( "Initialization and sharing of 'superModel' reverse relations from a 'leaf' child model" , function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );
scope.PetAnimal = Backbone.RelationalModel.extend({
subModelTypes: {
'dog': 'Dog'
}
});

scope.Flea = Backbone.RelationalModel.extend({});
scope.Dog = scope.PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
},
relations: [{
type: Backbone.HasMany,
key: 'fleas',
relatedModel: scope.Flea,
reverseRelation: {
key: 'host'
}
}]
});
scope.Poodle = scope.Dog.extend();

// Define the PetPerson after defining all of the Animal models. Include the 'owner' as a reverse-relation.
scope.PetPerson = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'pets',
relatedModel: scope.PetAnimal,
reverseRelation: {
type: Backbone.HasOne,
key: 'owner'
}
}]
});

// Initialize the models starting from the deepest descendant and working your way up to the root parent class.
var poodle = new scope.Poodle({
name: 'Mitsy'
});

var dog = new scope.Dog({
name: 'Spot'
});

var person = new scope.PetPerson({
pets: [ dog, poodle ]
});

ok( dog.get( 'owner' ) === person, "Dog has a working owner relation." );
ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );

var flea = new scope.Flea({
host: dog
});

var flea2 = new scope.Flea({
host: poodle
});

ok( dog.get( 'fleas' ).at( 0 ) === flea, "Dog has a working fleas relation." );
ok( poodle.get( 'fleas' ).at( 0 ) === flea2, "Poodle has a working fleas relation." );
});

test( "Initialization and sharing of 'superModel' reverse relations by adding to a polymorphic HasMany" , function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );
scope.PetAnimal = Backbone.RelationalModel.extend({
// The order in which these are defined matters for this regression test.
subModelTypes: {
'dog': 'Dog',
'fish': 'Fish'
}
});

// This looks unnecessary but it's for this regression test there has to be multiple subModelTypes.
scope.Fish = scope.PetAnimal.extend({});

scope.Flea = Backbone.RelationalModel.extend({});
scope.Dog = scope.PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
},
relations: [{
type: Backbone.HasMany,
key: 'fleas',
relatedModel: scope.Flea,
reverseRelation: {
key: 'host'
}
}]
});
scope.Poodle = scope.Dog.extend({});

// Define the PetPerson after defining all of the Animal models. Include the 'owner' as a reverse-relation.
scope.PetPerson = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'pets',
relatedModel: scope.PetAnimal,
reverseRelation: {
type: Backbone.HasOne,
key: 'owner'
}
}]
});

// We need to initialize a model through the root-parent-model's build method by adding raw-attributes for a
// leaf-child-class to a polymorphic HasMany.
var person = new scope.PetPerson({
pets: [{
type: 'poodle',
name: 'Mitsy'
}]
});
var poodle = person.get('pets').first();
ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );
});

test( "Overriding of supermodel relations", function() {
Expand Down

0 comments on commit bf6a2b5

Please sign in to comment.