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.