Skip to content

Instantly share code, notes, and snippets.

@mikeric
Created March 20, 2013 18:21
Show Gist options
  • Save mikeric/5207129 to your computer and use it in GitHub Desktop.
Save mikeric/5207129 to your computer and use it in GitHub Desktop.

Managing Nested Models and Collections in Backbone.js

One common pattern for nesting another model or collection inside a model is to assign it as a property on that object, for example, @messages on Mailbox as illustrated on the Backbone.js homepage.

class Mailbox extends Backbone.Model
  urlRoot: '/mailbox'

  initialize: ->
    @messages = new Messages
    @messages.url = @url() + '/messages'

The above implementation works fine if you always initialize a new Mailbox with an id and never instantiate or update the mailbox with it's messages in the same payload/object — which would be the case if your mailbox API also returns the messages in the JSON along with the mailbox. Here's what I'm talking about.

# /mailbox/1.json
{id: 1, messages: [{id: 1, …}, {id: 2, …}, {id: 3, …}]}

In this case, what we want is for that messages array to get applied to the @messages collection instead of being set as a model attribute. Thankfully Backbone provides a convenient way to intercept this data by overwriting the parse function.

class Mailbox extends Backbone.Model
  urlRoot: '/mailbox'

  initialize: ->
    @messages = new Messages
    @messages.url = @url() + '/messages'

  parse: (data) =>
    if data.messages?
      @messages.update data.messages
      delete data.messages

    data

Now data will pass through the parse function when we fetch, save, or in our case, instantiate the model with data (you just need to pass {parse: true} when instantiating the model).

new Mailbox(data, parse: true)

Oddly enough, this complains that @messages is undefined, even though we've defined it in initialize (note that it is defined when we do a fetch or save, just not on model creation). This is because parse gets run before initialize does, and since @messages is defined inside our initialize, it hasn't been defined yet when parse gets run.

The solution is to define a custom constructor for Mailbox that creates the @messages collection on the object so that we have access to it inside our parse function.

class Mailbox extends Backbone.Model
  urlRoot: '/mailbox'

  constructor: ->
    @messages = new Messages
    @messages.url = @url() + '/messages'
    super

  parse: (data) =>
    if data.messages?
      @messages.update data.messages
      delete data.messages

    data

We're pretty close now. It's complaining that @id is undefined, which actually makes sense because @url() requires @id to be set, and since we're in the constructor, Backbone hasn't had the chance to assign it yet.

Part of the solution here is to not set an explicit url on the nested collections — assume that you probably won't have access to all the ids to construct a proper url for that nested collection at every point in your app, and define url as a function on the collection instead. Of course, this means it will need to hold reference to the parent model (at the constructor level) for it to be able to generate the nested url. If a parent model isn't provided, we can fall back to generating a non-nested (root?) url for the collection.

class Messages extends Backbone.Collection
  constructor: ->
    @parent = arguments[1]?.parent
    super

  url: =>
    (@parent?.url() or '') + '/messages'

class Mailbox extends Backbone.Model
  urlRoot: '/mailbox'

  constructor: ->
    @messages = new Messages [], parent: @
    super

  parse: (data) =>
    if data.messages?
      @messages.update data.messages
      delete data.messages

    data

Using the above implementation, you can fetch, sync and create a model with nested collection data and be rest-assured that it will properly update your nested collection.

Another plus-side to doing things this way is the maintainability win — since changes to the parent's url will be reflected in the nested collection's url without needing to update them directly. Likewise, changing the parent of the nested collection to an entirely different model will be reflected in the collection's url.

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