Skip to content

Instantly share code, notes, and snippets.

@geddski
Created January 14, 2012 05:08
Show Gist options
  • Save geddski/1610397 to your computer and use it in GitHub Desktop.
Save geddski/1610397 to your computer and use it in GitHub Desktop.
helper function for nesting backbone collections.

nestCollection function makes it easy to nest collections in Backbone.js

Models often need nested collections. Given the example in the FAQ:

var Mailbox = Backbone.Model.extend({
  initialize: function() {
    this.messages = new Messages;
  }
});

var myBox = new Mailbox;

Let's say you're using a NoSQL DB like mongodb, and are pulling down all the data for a given mailbox on page load. So your model's toJSON() output looks like this:

{
mailboxName: "432 West",
messages: [
  {from: "Mom", title: "Learn your JavaScript"},
  {from: "City Hall", title: "Your dogs bark too loud"}
]
}

Now in your app you change or add a message:

var momsMessage = myBox.messages.at(1);
momsMessage.set({title: "return to sender"});

Well now we have a big problem. myBox.toJSON() contains the original data, not the updated data. Your 'return to sender' title won't get saved to the server unless you override Mailbox's toJSON function. What a pain, bloating all our models with overridden toJSON functions and change events.

Backbone should be much smarter about nesting collections. The model's underlying data should point to the same data as the nested collection. This is easy with JS thanks to reference types (objects, arrays, etc.) I created a simple static function called nestCollection. You pass it the model, the attribute name, and collection instance. It returns the collection instance for convenience. Example usage:

var Mailbox = Backbone.Model.extend({
  initialize: function() {
    this.messages = nestCollection(this, 'messages', new Messages(this.get('messages')));
  }
});

var myBox = new Mailbox;

Now when you render myBox in a template or save() it to the server, it will always have the right data, all without overriding toJSON or any other trickery.

The only real complaint I've heard about backbone is that it's complex and difficult to nest collections. Problem solved.

Before

When you create a nested model like so this.messages = new Message(this.get('messages')) you create a new object that is separate from your model's underlying data. It now looks like this: Before

Hence the problem: you update your nested collection, and your model's data is out of date because they are different objects.

After

nestCollection() changes the model's attribute data to point to the nested collection's data:

after

Also whenever the nested collection adds/removes an item, that same item data gets added back to the model data. It's simple and elegantly solves the nesting problem.

function nestCollection(model, attributeName, nestedCollection) {
//setup nested references
for (var i = 0; i < nestedCollection.length; i++) {
model.attributes[attributeName][i] = nestedCollection.at(i).attributes;
}
//create empty arrays if none
nestedCollection.bind('add', function (initiative) {
if (!model.get(attributeName)) {
model.attributes[attributeName] = [];
}
model.get(attributeName).push(initiative.attributes);
});
nestedCollection.bind('remove', function (initiative) {
var updateObj = {};
updateObj[attributeName] = _.without(model.get(attributeName), initiative.attributes);
model.set(updateObj);
});
return nestedCollection;
}
@nagyv
Copy link

nagyv commented Feb 18, 2012

really nice, there is just one more feature I'll try to add to it, to store the attributes as simple id references, instead of the full models, and fetch the model data using the nested collection

thanks for your code

@geddski
Copy link
Author

geddski commented Feb 18, 2012

@nagyv glad you find it useful. forks are welcome.

@lagartoflojo
Copy link

Thanks for this, I've been using it in my app. I made a CoffeeScript version here: https://gist.github.com/1821349

@geddski
Copy link
Author

geddski commented Feb 23, 2012

@lagartoflojo you're welcome. If you find any improvements in your CS version please contribute back! Thanks

@lagartoflojo
Copy link

The only major changes I made to your version was extending the Backbone.Model prototype with the nestCollection function instead of making it a global function, and getting rid of the first "this" parameter =)

@geddski
Copy link
Author

geddski commented Feb 23, 2012

Yeah that's how I had it back when I submitted it as a pull request to Backbone.

@subimage
Copy link

subimage commented Mar 1, 2012

Simple, yet awesome patch. +1 on adding it into Model.prototype. It's silly this isn't included in the original framework.

@geddski
Copy link
Author

geddski commented Mar 1, 2012

@subimage thanks. Unfortunately Jeremy didn't want backbone to be perscriptive about this kind of thing, so the pull request wasn't accepted. Did add it to the extensions wiki though.

@haberman
Copy link

haberman commented Mar 3, 2012

Shouldn't you call nestCollection in set() instead of initialize(), so the collection will be properly nested when the client calls fetch() or set()?

var Mailbox = Backbone.Model.extend({
  set: function(attributes, options) {
    var ret = Backbone.Model.prototype.set.call(this, attributes, options);
    if (attributes.messages)
      this.messages = nestCollection(this, 'messages', new Messages(this.get('messages')));
    return ret;
  }   
});

@GeReV
Copy link

GeReV commented Mar 21, 2012

I suggest also adding a line that will enhance parse to cope with fetched data, something along these lines:

function nestCollection(model, attributeName, nestedCollection) {
  // ...
  model.parse = function(response) {
    if (response && response[attributeName]) {
      model[attributeName].reset(response[attributeName]);
    }
    return Backbone.Model.prototype.parse.call(model, response);
  }
  // ...
}

This isn't perfect, though, since it doesn't take multiple nested collections into consideration. Still haven't thought that part completely through.

Maybe something like return model.prototype.parse.call(model, response) will suffice, not sure.

@alltom
Copy link

alltom commented Apr 8, 2012

Thanks so much for this gist!

I actually had the problem @haberman and @GeReV are trying to code around, though: in my case, save() was calling set() with the attributes from the server response.

Since there are references to my nested collections throughout my app (attached to various views), I couldn't just recreate them. Instead, I use reset() with the new values, and added a reset handler to nestCollection that re-does the attribute linkages.

@fredkelly
Copy link

@alltom don't suppose you have an example of this?

@nicodeg87
Copy link

Hi!

This fix is not necessary.

Please, use this.set('messages') instead this.messages on initialize method for set Collections or Models. This allow JSON stringify works correctly. ( Test with: "JSON.stringify(model_instance)" ).

@kaxline
Copy link

kaxline commented Nov 12, 2012

I tweaked this a little to use 'on' instead of 'bind' and _.filter instead of _.without. _without wasn't correctly removing my attributes with nested objects. i haven't tested this thoroughly but it works for now. if anyone is having trouble with removing models from the collection attribute, give this a try.

also, if anyone sees something problematic here, please let me know.

function nestCollection (model, attributeName, nestedCollection) {
    //setup nested references
    for (var i = 0; i < nestedCollection.length; i++) {
      model.attributes[attributeName][i] = nestedCollection.at(i).attributes;
    }
    //create empty arrays if none

    nestedCollection.on('add', function (initiative) {
        var tempArray = [];
        if (!model.get(attributeName)) {
            model.attributes[attributeName] = [];
        }
        tempArray = model.get(attributeName).push(initiative.attributes);
        model.set({
            attributeName:tempArray
        });
    });

    nestedCollection.on('remove', function (initiative) {
        var updateObj = {};
        updateObj[attributeName] = _.filter(model.get(attributeName), function(attrs) {
            return attrs._id !== initiative.attributes._id;
        });
        model.set(updateObj);
    });
    return nestedCollection;
}

@kaxline
Copy link

kaxline commented Nov 14, 2012

sorry, posted that last bit too soon. should be:

function nestCollection (model, attributeName, nestedCollection) {
//setup nested references
for (var i = 0; i < nestedCollection.length; i++) {
  model.attributes[attributeName][i] = nestedCollection.at(i).attributes;
}
//create empty arrays if none

nestedCollection.on('add', function (initiative) {
    if (!model.get(attributeName)) {
        model.attributes[attributeName] = [];
    }
    model.get(attributeName).push(initiative.attributes);
});

nestedCollection.on('remove', function (initiative) {
    var updateObj = {};
    updateObj[attributeName] = _.filter(model.get(attributeName), function(attrs) {
        return attrs._id !== initiative.attributes._id;
    });
    model.set(updateObj);
});
return nestedCollection;

}

@hpbruna
Copy link

hpbruna commented Jan 14, 2013

The API where I am building on, supplies json data like this:

{"uid":"2","name":"testuser","mail":"testuser@anurb.nl","theme":"","signature":"","signature_format":"filtered_html","created":"1357228247","access":"0","login":"0","status":"1","timezone":"Europe/Berlin","language":"","picture":null,"init":"testuser@anurb.nl","data":false,"roles":{"2":"authenticated user"},"field_goes_to_events":{"und":[{"target_id":"4"}]},"rdf_mapping":{"rdftype":["sioc:UserAccount"],"name":{"predicates":["foaf:name"]},"homepage":{"predicates":["foaf:page"],"type":"rel"}}}

I want to nest the collection under field_goes_to_events":{"und":[{"target_id":"4", "target_id":"6", "target_id":"12"}]}
But there is an extra language key: 'und' and I only need the target_id's.

Any ideas how to go about this? Thanx in advance.

@paynecodes
Copy link

Is this still relevant? I'm new to Backbone, and need to create a new collection from within an already fetched collection. This still the best way?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment