Backbone: What is the best way to model a Collection inside of a Model?

Created on 4 Nov 2010  ·  11Comments  ·  Source: jashkenas/backbone

My team, is working with an interesting data schema. Basically what we have is a document that has a name and version and an array of items that belongs to it. The items themselves don't have ids associated with them because they belong to the larger model but the items can be added/edited/deleted from the main document. The model looks something like this:
{
name : "Test",
version : 1,
items : [
{name : "Item 1",
position : 0},
{name : "Item 2",
position : 1}]
}

It would be great to use a Collection for the underlying items in the Model but whenever the Collection gets updated, the Model should post back to the server with it's url. What is the best way to model this in Backbone? I'll be happy to post more in a gist if more info is needed.

question

Most helpful comment

@eranation: nope, should be pretty much the same. When using this pattern, I like to keep the items out of my attributes hash so I don't have to keep them in sync. This requires that you pull the items object out of the response for parsing, and add it back in for serializing (see below). Aside from that, you might want to put the logic that @rsim put in the initialize in your constructor method instead, and use on instead of bind (which is semi-deprecated).

I find it's much easier to have attributes be only a shallow hash (no nested collections, models, etc.), where possible.

var Document = Backbone.Model.extend({
  constructor: function() {
    this.items = new ItemSet(null, {document: this});
    this.items.on('change', this.save, this);
    Backbone.Model.apply(this, arguments);
  },
  parse: function(resp) {
    this.items.set(resp.items, {parse: true, remove: false});
    delete resp.items;
    return resp;
  },
  toJSON: function() {
    var attrs = _.clone(this.attributes);
    attrs.items = this.items.toJSON();
    return attrs;
  }
});
var ItemSet = Backbone.Collection.extend({
  model: Item,
  initialize: function(models, options) {
    this.document = options.document;
  }
});
var Item = Backbone.Model.extend({
  // access document with this.collection.document
});
var document1 = new Document({
  name: "Test",
  version: 1,
  items: [
    {name : "Item 1", position : 0},
    {name : "Item 2", position : 1}
  ]
});

This seems to work well for even deeply-nested schemas. (see the docs)

All 11 comments

I'd say you have two main options... The first is to leave the items as a vanilla attribute. Backbone uses a deep equality check when attributes change, so if an inner item is updated, the document will know about it.

The other option is to pull the items out of the document and attach them as models in their own right, inside of a collection stuck on the document (we do something along these lines at DocumentCloud). For example (roughly speaking):

var Document = Backbone.Model.extend({
  initialize: function() {
    this.items = new ItemSet();
    this.items.bind('change', this.save);
  }
});

Do either of those work well for you?

I would recommend also to add "document" property back to Document object which could be accessed when needed from items:

var Document = Backbone.Model.extend({
  initialize: function() {
    this.items = new ItemSet(this.get('items'), {document: this});
    this.items.bind('change', this.save);
  }
});
var ItemSet = Backbone.Collection.extend({
  initialize: function(models, options) {
    this.document = options.document;
  }
});
var Item = Backbone.Model.extend({
  // access document with this.collection.document
});
var document1 = new Document({
  name: "Test",
  version: 1,
  items: [
    {name : "Item 1", position : 0},
    {name : "Item 2", position : 1}
  ]
});

I believe binding the items' change event to the document save is the missing piece. Although, the adding of the document to the ItemSet looks to be very helpful as well. We'll try this today and I'll let you guys know how it turns out.

This worked out well. The biggest issue we are having now is with mongoid. Thanks for the help guys.

How would I go about saving all the items at once? I don't want to make a request each time an item is changed but I want to save them all with a single request on the user's action. I was thinking about collecting everything when the document is saved and updating the document's 'items' attr. Is that a good solution?

As long as your items are set in the document, then when you do a document.save() the items will be sent up to the server as well.

But let's say if I add an item to the collection document1.items at runtime it doesn't get added to the 'items' attribute of document1 also automagically. So if I then do a document1.save(), the new model I added to the collection won't be sent to the server. I can't see how the changes to the collection could propagate in the model's attributes that are sent with save.

So, here is how we're handeling it: the document has a default items array. On initialization, in an overloaded set method, I create a new items collection from the attributes and set that on the document.

class Document extends Backbone.Model
  defaults:
    items: []

  set: (attrs, options) ->
    items = attrs['items']
    if _( items ).isArray()
      if _( items ).isEmpty()
        attrs['items'] = new DocumentItemsCollection
        newItem = new Item
        attrs['items'].add(newItem, { silent: true })
      else
        attrs['items'] = new DocumentItemsCollection items

At that point you just deal with the items collection methods with 'get', 'set', 'add' and 'remove'. You don't mess with the dot notation. I even have methods on my Document class called addItem and deleteItem to fire change events on the document itself. When you do a save() on the document it'll call toJSON on your item collection.

Honestly, this is just a simple case for our documents and we have even deeper sub-documents. Dealing with this amount of complexity with backbone, and overloading several of the methods on the models, is a real big pain in the ass. We are now looking at replacing backbone with sproutcore in the future.

If you are having to deal with really complex documents then I would suggest looking at ExtJS or sproutcore. Backbone is great for a small project with simple models but falls apart pretty quickly when the objects/interactions start ramping up.

Are there any new "best practices" for this for 1.0?

@eranation: nope, should be pretty much the same. When using this pattern, I like to keep the items out of my attributes hash so I don't have to keep them in sync. This requires that you pull the items object out of the response for parsing, and add it back in for serializing (see below). Aside from that, you might want to put the logic that @rsim put in the initialize in your constructor method instead, and use on instead of bind (which is semi-deprecated).

I find it's much easier to have attributes be only a shallow hash (no nested collections, models, etc.), where possible.

var Document = Backbone.Model.extend({
  constructor: function() {
    this.items = new ItemSet(null, {document: this});
    this.items.on('change', this.save, this);
    Backbone.Model.apply(this, arguments);
  },
  parse: function(resp) {
    this.items.set(resp.items, {parse: true, remove: false});
    delete resp.items;
    return resp;
  },
  toJSON: function() {
    var attrs = _.clone(this.attributes);
    attrs.items = this.items.toJSON();
    return attrs;
  }
});
var ItemSet = Backbone.Collection.extend({
  model: Item,
  initialize: function(models, options) {
    this.document = options.document;
  }
});
var Item = Backbone.Model.extend({
  // access document with this.collection.document
});
var document1 = new Document({
  name: "Test",
  version: 1,
  items: [
    {name : "Item 1", position : 0},
    {name : "Item 2", position : 1}
  ]
});

This seems to work well for even deeply-nested schemas. (see the docs)

@akre54 thanks, this is a great example, much appreciated

Was this page helpful?
0 / 5 - 0 ratings

Related issues

miguelpayet picture miguelpayet  ·  9Comments

spawnedc picture spawnedc  ·  9Comments

zowers picture zowers  ·  11Comments

g00fy- picture g00fy-  ·  9Comments

gfranko picture gfranko  ·  18Comments