In my application, I am trying to use Firebase to store the real time data based on backbone framework.
The problem goes like this:
I have a sub level model and collection, which are both general backbone model and collection.
var Todo = Backbone.Model.extend({
defaults: {
title: "New Todo",
completed : true
}
});
var Todocollection = Backbone.Collection.extend({
model: Todo,
initialize: function() {
console.log("creating a todo collection...");
},
});
And then there is a high level model, which contains the sublevel collection as an attribute.
var Daymodel = Backbone.Model.extend({
defaults : {
day: 1,
agenda : new Todocollection()
}
});
and then for the higher level collection, I will firebase collection
var DayCollection = Backbone.Firebase.Collection.extend({
model: Daymodel
});
So far I can add data to the higher level collection correctly, which has a day
attribute and an agenda
attribute (which should be a TodoCollection
).
The issue is when I try to add data to the sub-level collections, it can't work well.
this.collection.last()
.get("agenda")
.add({
title: this.input.val(),
completed: false
});
The above code will be inside the View part. And this.collection.last()
will get the last model. get("agenda")
should be the collection object.
But it can't work. The error shows that this.collection.last(...).get(...).add
is not a function.
After debugging I found that this.collection.last().get("agenda")
returns a general JS object instead of collection object.
I further debugged that if I use backbone collection as the outer collection DayCollection
. Everything can go well.
How to solve such problem?
When you fetch, or create a new Daymodel
which I assume looks like this:
{
day: 1,
agenda : [{
title: "New Todo",
completed : false
}, {
title: "other Todo",
completed : false
}]
}
The default agenda
attribute which was a Todocollection
at first gets replaced by a raw array of objects. Backbone doesn't know that agenda
is a collection and won't automagically populates it.
This is what Backbone does with the defaults
at model creation (line 401):
var defaults = _.result(this, 'defaults'); attrs = _.defaults(_.extend({}, defaults, attrs), defaults); this.set(attrs, options);
_.extend({}, defaults, attrs)
puts the defaults
first, but then, they're overwritten by the passed attrs
.
Below are three solutions to accomplish this. Use only one of them, or create your own based on the followings.
Keep the Todocollection
out of the Daymodel
model and only create the collection when you need it, like in the hypothetical DayView
:
var DayView = Backbone.View.extend({
initialize: function() {
// create the collection in the view directly
this.agenda = new Todocollection(this.model.get('agenda'));
},
/* ...snip... */
});
Then, when there are changes you want to persist in the model, you just put the collection models back into the Daymodel
:
this.model.set('agenda', this.collection.toJSON());
Instead of an attribute, you could make a function which lazily create the collection and keeps it inside the model as a property, leaving the attributes
hash clean.
var Daymodel = Backbone.Model.extend({
defaults: { day: 1, },
getAgenda: function() {
if (!this.agenda) this.agenda = new Todocollection(this.get('agenda'));
return this.agenda;
}
});
Then, the model controls the collection and it can be shared easily with everything that shares the model already, creating only one collection per instance.
When saving the model, you still need to pass the raw models back into the attributes
hash.
You can accomplish what you're already trying to do with small changes.
Never put objects into the defaults
...without using a function returning an object instead.
var Daymodel = Backbone.Model.extend({
defaults: function() {
return {
day: 1,
agenda: new Todocollection()
};
},
});
Otherwise, the agenda
collection would be shared between every instances of Daymodel
as the collection is created only once when creating the Daymodel
class.
This also applies to object literals, arrays, functions (why would you put that in the defaults
anyway?!).
Ensure it's always a collection.
var Daymodel = Backbone.Model.extend({
defaults: { day: 1, },
initialize: function(attrs, options) {
var agenda = this.getAgenda();
if (!(agenda instanceof Todocollection)) {
// you probably don't want a 'change' event here, so silent it is.
return this.set('agenda', new Todocollection(agenda), { silent: true });
}
},
/**
* Parse can overwrite attributes, so you must ensure it's a collection
* here as well.
*/
parse: function(response) {
if (_.has(response, 'agenda')) {
response.agenda = new Todocollection(response.agenda);
}
return response;
},
getAgenda: function() {
return this.get('agenda');
},
setAgenda: function(models, options) {
return this.getAgenda().set(models, options);
},
});
Ensure it's serializable.
var Daymodel = Backbone.Model.extend({
/* ...snip... */
toJSON: function(options) {
var attrs = Daymodel.__super__.toJSON.apply(this, arguments),
agenda = attrs.agenda;
if (agenda) {
attrs.agenda = agenda.toJSON(options);
}
return attrs;
},
});
This could easily apply if you put the collection in a model property as explained above.
Avoid accidentally overriding the agenda
attribute.
This goes alongside with point 2 and that's where it's getting hard as it's easy to overlook, or someone else (or another lib) could do that down the line.
It's possible to override the save
and set
function to add checks, but it gets overly complex without much gain in the long run.
I talked about avoiding it inside a model completely, or lazily creating it. That's because it can get really slow if you instantiate a lot of models and slower if each models is nested multiple times (models which have a collection of models, which have other collections of models, etc).
When creating it on demand, you only use the machine resources when you need it and only for what's needed. Any model that's not on screen now for example, won't get their collection created.
Maybe it's too much work to get this working correctly, so a complete solution might help and there are a couple.