diff --git a/backbone-relational.js b/backbone-relational.js index 17e7f9af..fecd7807 100755 --- a/backbone-relational.js +++ b/backbone-relational.js @@ -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; } }, diff --git a/test/tests.js b/test/tests.js index 29d0ade9..f55540d4 100644 --- a/test/tests.js +++ b/test/tests.js @@ -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({ @@ -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() { @@ -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: [{ @@ -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 ); @@ -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', @@ -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() {