Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the best way to model a Collection inside of a Model? #56

Closed
jonathan opened this issue Nov 3, 2010 · 11 comments
Closed

What is the best way to model a Collection inside of a Model? #56

jonathan opened this issue Nov 3, 2010 · 11 comments
Labels

Comments

@jonathan
Copy link

jonathan commented Nov 3, 2010

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.

@jashkenas
Copy link
Owner

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?

@rsim
Copy link
Contributor

rsim commented Nov 4, 2010

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}
  ]
});

@jonathan
Copy link
Author

jonathan commented Nov 4, 2010

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.

@jonathan
Copy link
Author

jonathan commented Nov 5, 2010

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

@nickdima
Copy link

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?

@jonathan
Copy link
Author

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.

@nickdima
Copy link

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.

@jonathan
Copy link
Author

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.

@eranation
Copy link

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

@akre54
Copy link
Collaborator

akre54 commented Mar 29, 2013

@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)

@eranation
Copy link

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

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants