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
dataNow 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
dataWe'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
dataUsing 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.