backbone.jsbackbone-relational

Does Backbone Relational have problems when two separate (but "related") collections contain models with identical Ids?


I'm using Backbone.js, Marionette.js and Backbone-relational on a page that displays lists of users. Each user is identified as either "pending" or "active." A "pending" user is one that has been sent an invitation to join (the party) but has not accepted; "active" are those who have confirmed. The list of pending users is fetched by one API call and the list of active users is fetched by another.

So here's the situation:

A user, let's call him "Miguel", is fetched into the collection of active users. But if a "pending" user who has the same ID as Miguel is fetched to the collection of pending users, Miguel will receive the attribute "Status":"pending". I can verify that Miguel is not fetched into (or passed to the parse function of--) the collection of pending users.

So I'm wondering whether Backbone relational has problems having relationships to two collections whose model IDs sometimes match each other.

To illustrate:

If Collection of active users: { Id: 22, 'FirstName': 'Miguel' }

And Collection of pending users: { Id: 22, 'FirstName': 'Betty', 'Status': 'Pending' }

Then Miguel winds up looking like: { Id: 22, 'FirstName': 'Miguel', 'Status': 'Pending' }

Here's the code for my model and collection:

/**
 *  "NV" is a base class we created for this application. It handles .save(), .toPatchJSON(), tracks changed attrs and other such functions.
 */

/** 
 *  This is the collection of "pending" users
 */
var PendingUsers = NV.Collection.extend({

    model: User,

    initialize: function(models, options) {
        Backbone.Collection.prototype.initialize.call(this,models,options);
        this.org = options.org;
    },

    url: function() {
        return "/api/organization/" +  this.org.get("Id") + "/invites";
    },

    parse: function(resp, options) {
        // "Miguel" never shows up in this function yet obtains the "Pending" Status property.
        return _.map(resp, function(invite) {
            return {
                Id: invite.Id,
                FirstName: invite.FirstName,
                LastName: invite.LastName,
                Email: invite.Email,
                InvitationTime: invite.InvitationTime + "+00:00",
                Status: 'Pending'
            }
        });
    }
});


/** 
 *  This is the collection of "active" users
 */
var ActiveUsers = NV.Collection.extend({
    model: User,

    initialize: function(models, options) {
        Backbone.Collection.prototype.initialize.call(this,models,options);
        this.org = options.org;
    },

    url: function() {
        return "/api/organization/" +  this.org.get("Id") + "/users";
    }
});


/**
 *  The collections of active and pending users are relations of the "Organization" model. This is what's passed to the View that renders the list of users.
 */
var Organization = NV.Model.extend({
    defaults: {
        Name: ''
    },
    relations: [
        {
            type: Backbone.HasMany,
            key: 'Users',
            relatedModel: User,
            collectionType: ActiveUsers,
            collectionOptions: function(org) {
                return {'org': org };
            }
        },
            {
                type: Backbone.HasMany,
                key: 'InvitedUsers',
                relatedModel: User,
                collectionType: PendingUsers,
                collectionOptions: function(org) {
                    return {'org': org };
                }
            }
    ],

    urlRoot: "/api/organization"
});

return Organization;


/**
 *  Both pending and active collections build from the User model, so here it is
 */

var User = NV.Model.extend({
    schema: {
        Name: 'Text',
        Email: { validators: ['required', 'email'] },
        password: 'Password'
    },

    defaults: {
        FirstName: '',
        LastName: '',
        Email: ''
    },


    fetch: function(options) {
        this.me = options.me || false;
        NV.Model.prototype.fetch.call(this,options);
    },

    avatar: function(options){
        return  "/api/user/" + this.get("Id") + "/avatar";
    },

    url: function() {
        if (this.me) return "/api/user/me";
        return  "/api/user/" + this.get("Id");
    }


});

return User;

Let me know if I've failed to include important info. in this question! And thanks!


Solution

  • You are right, Backbone.Relational has a problem with that. Behind the scenes it's keeping a global collection of all models of given type - in this case all Users. What happens when you fetch is actually Backbone behavior. It recognizes that you are feching into existing collection ( the global one ), finds a model with same ID and attempts to merge data, which results in "Status" attribute on the "Miguel" model.

    This is usually a desired functionality as keeping multiple models of same type with same id often indicates some problem or suboptimal data loading.

    A quick workaround would be to create additional User 'class', like PendingUser, by extending User.