Skip to content

Instantly share code, notes, and snippets.

@crueber
Forked from lagartoflojo/nesting.coffee
Last active December 17, 2015 01:29
Show Gist options
  • Save crueber/5528407 to your computer and use it in GitHub Desktop.
Save crueber/5528407 to your computer and use it in GitHub Desktop.
BackboneJS does not have a way to directly nest collections within a model. This adds that capability.

nestCollection on Backbone.Model

This should make it easier to nest collections in backbone.js

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

Mailbox = Backbone.Model.extend
  initialize: -> @messages = new Messages

inbox = 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:

momsMessage = inbox.messages.at(1)
momsMessage.set(title: "return to sender")

Well now we have a big problem. inbox.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 on the Model prototype. You pass it the attribute name, and collection instance. It returns the collection instance for convenience. Example usage:

Mailbox = Backbone.Model.extend
  initialize: ->
    @on 'change:messages', =>
      @messages = @nestCollection('messages', new Messages(@get('messages')))

inbox = new Mailbox

Now when you render inbox 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 @messages = new Message(@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.

# Description: Add the nestCollection method to the Backbone.Model prototype.
#
# This method accepts an attribute name on the model you're currently referencing,
# along with a collection that you would like to use the attributes of. Making it
# easier to bubble events between a collection/model.
#
# TODO: Fix comparator issues that could be introduced if the dependent collection changes sort order.
Backbone.Model::nestCollection = (attribute_name, collection_to_nest) ->
# Make sure that there is an array available in the attribute_name that hsa been passed.
@attributes[attribute_name] = [] if !@attributes[attribute_name]
# Go over each item in the collection, and *directly* access the attributes to
# set them up in a manual array on the current model. This means that the collection
# itself is not specifically set on the object, just the referenced data.
_.each collection_to_nest, (nest_item, index) ->
@attributes[attribute_name][index] = nest_item.attributes
# Event binding to add entries to the model's attribute when they are added to the collection
collection_to_nest.on 'add', (bound_collections_model) =>
@attributes[attribute_name].push(bound_collections_model.attributes)
# Event binding to remove entries from the model's attribute when they are pulled out
# of the collection
collection_to_nest.on 'remove', (bound_collections_model, bound_collection, options) =>
@attributes[attribute_name] = _.without @attributes[attribute_name] bound_collections_model.attributes
# No matter whether a collection is added to, removed from, or changed, we are always going to fire a
# change event on this model object.
collection_to_nest.on 'add remove change', =>
@trigger 'change', @
# Return the collection afterwards for chaining.
collection_to_nest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment