If I have a collection of objects, but also want to store some higher-level information about those objects, is it appropriate to add some model behavior to the collection?
In my situation, I'm looking for a collection of application paths to have a bool field called 'curPath'. If it's changed, the collection should set a flag that indicates the current page. This way outside observers only have to observe one field, not every model in the path collection.
Here's what that might look like:
var PathModel = Backbone.Model.extend({})
var PathCollection = Backbone.Collection.extend({
initialize: function(){ this.model = PathModel }
})
// I want to be able to set observable properties on the collection, so...
var PathManager = _.extend(Backbone.Model, PathCollection)
// Then maybe I can do something like this?
PathManager.each(function(pathModel){
pathModel.on('change:curPath', function(m, value, o){
// I mean for 'this'.set to point to the instance of the PathManager
if (value === true){ this.set('curPath', pathModel.get('id')) }
}, this)
}, this)
Is it appropriate to add observable behavior to a collection (collection+model > model), or do I need to add a wrapping model to the whole thing (model > collection > model), or is there some other solution?
As some of the methods on Model and Collection have the same names that will cause conflicts and possible problems with updating to future versions of Backbone could occur I would advise against this pattern. Rather then doing that create a PathManager class and have PathCollection be initialized and set it as paths property on this model. If from what you are saying
var PathManager = Backbone.Model.extend({
initialize: function() {
this.paths = new PathCollection();
}
});
var pathManager = new PathManager();
pathManager.paths.add({ /* new path */ });
pathManager.on('change', doSomething);
pathManager.paths.on('add', doSomethingElse);
I'd do it something like this
Related
I have a case where I am creating multiple views for a single model. When I close a view, I am removing the model from the collection.
But in case of multiple views, there are other views that depend on the model.
So, how can I know when there are no views depending on the model? When should I destroy the model in case of multiple views?
Generally speaking the lifecycle of a view shouldn't cause a model/collection to be modified, but assuming you have a good reason for it.
Here is a slight improvement on Kenny's answer, I'd suggest to have dependentViews as an array on the object, not a value in the attributes (if Views aren't persisted, best not persist view dependencies).
var myModel = Backbone.Model.extend({
initialize: function() {
this.dependentViews = [];
},
addDependentView: function(view) {
this.dependentView.push(view);
},
closeDependentView: function(view) {
this.dependentViews = _.without(this.dependentViews, view);
if (_.isEmpty(this.dependentViews)) {
//remove model from collection
}
}
})
var view1 = Backbone.View.extend({
initialize: function() {
this.model.addDependentView(this);
}
})
var view2 = Backbone.View.extend({
initialize: function() {
this.model.addDependentView(this);
}
})
...
onCloseView: function() {
this.model.closeDependentView(this);
}
Also an array of objects might come in handy for the dependency list, that way you could if necessary in future, make calls from the model to the dependent views.
Another possible solution might be to use the internal event listener register as a means of tracking any objects listening to the model. But that would be more involved and would depend on internal functionality within Backbone.
Though this is not a proper way but still you can achieve this following
Declare dependentViews in your model's defaults
var myModel = Backbone.Model.extend({
defaults: function() {
return {
dependentViews: 0
};
}
});
In initialization of each view, increment the dependentView
var view1 = Backbone.View.extend({
initialize: function() {
this.model.set("dependentViews",
this.model.get("dependentViews") + 1);
}
});
var view2 = Backbone.View.extend({
initialize: function() {
this.model.set("dependentViews",
this.model.get("dependentViews") + 1);
}
});
On close of view decrement dependentViews and on each view destroy, just check the value of dependentViews. If it is 0, remove the model from the collection.
onCloseView: function() {
this.model.set("dependentViews",
this.model.get("dependentViews") - 1);
if (this.model.get("dependentViews") === 0) {
//remove model from collection
}
}
Having the number of views inside the data model as suggested in other answers is not a good idea.
I have a case where I am creating multiple views for a single model
So you have a "place" where you create views, and destroy these views. This could be a parent view, controller, router instance etc.
If you don't have one then you need one.
You must be having references to these view instances for future clean up.
If you don't have it then you need it.
When you're destroying a view just check the remaining number of instance, if there are none then remove the model (If the view removes itself in response to some user action then it needs to notify the parent of removal).
This is something that should take place outside the view instances and model.
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?
Why the default collection attribute is not a collection anymore?
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.
How to use a collection within a model?
Below are three solutions to accomplish this. Use only one of them, or create your own based on the followings.
Easiest and most efficient way is don't.
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());
Put the collection into a property of the model
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.
A collection inside the attributes
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.
What's the cons of collection in models?
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.
Out of the box solutions
Maybe it's too much work to get this working correctly, so a complete solution might help and there are a couple.
Backbone relational
backbone-nested
backbone-nested-models
backbone-deep-model
If i want to add a custom property to my backbone model, is this the best was to do this? Is there a better way or a completely different approach to the functionality i want to achieve?
var myModel = Backbone.Model.extend({
defaults:{
monthly_amount: 100
},
initialize: function(model, options){
var m = this;
Object.defineProperty(this,"yearly_amount",{
get: function() {
return (m.get("monthly_amount") * 12);
},
set: function(value) {
m.set("monthly_amount", (value/12) );
}
});
}
});
Thanks!
Edit: The property is just "virtual", i do not want it to be within the model attributes when saving the model to the server.
So the general problem here is often referred to as "computed properties" and there are plugins for backbone that provide this (see below). Backbone uses get/set method calling style as opposed to defineProperty style so your approach would make the value computation not transparent to views and thus be a pretty strong departure from backbone's design. A plugin that maintains the proper get/set and change interfaces is going to maintain the basic Model API so the view doesn't have to treat this particular model attribute differently.
See also the backbone wiki of plugins.
Available plugins:
Backbone.ModelMorph
Backbone.ComputedModel
Backbone.Spark
Backbone.ComputedFields
When you set your property to defaults oject, it goes to attributes, but it doesn't suit us.
Since Backbone model is an regular object, you can access to it's properties as to objects properties:
var myModel = Backbone.Model.extend({}),
model = new myModel();
model.monthly_amount = 50;
console.log(model.monthly_amount)
or create setter and getter like this:
var myModel = Backbone.Model.extend({
setMonthlyAmount: function (value) {
this.monthly_amount = value;
},
getMonthlyAmount: function () {
return this.monthly_amount;
}
});
//access with getter/setter
var model = new myModel();
model.setMonthlyAmount(20);
var result = model.getMonthlyAmount();
console.log(result);
Working example on jsfiddle
I have the following in test.html:
<script>
var Foo = Backbone.Model.extend({
initialize: function(options) {
console.log('hello!');
}
});
var Bar = Backbone.Collection.extend({
model: Foo
});
var m = new Bar({});
</script>
As it turns out, when the variable m is initialized, the initialize function of Foo is called. Thus, in the Firebug console, I get 'hello!'. When I comment out the line:
model: Foo,
There is no 'hello!' in the console output. Thus, declaring a model for a collection calls that model's initialize function. I think this behavior is a bit silly. I haven't read through backbones code, but is there a reason for this?
Well, there's nothing wrong with the behaviour of the code.
When you pass the model in the collection definition, you specify that every model in that collection will be of type Foo.
When you initialize the collection new Bar({}), you pass a model to the collection (although, as #alexanderb stated, I think that the collections expects an array as a first argument) and it initializes it, thus outputting 'hello!'.
For example, if you do not pass any models to the collection constructor :
new Bar();// no console output
there will be no console output, and on the other hand, if you would pass an array of objects, then the collection would initialize all of the provided models :
new Bar([{},[},{}]);// will output 'hello!' 3 times
I believe the constuctor of collection is expecting the array of models. So, what you should do is:
var collection = new Bar( [ {} ] );
There, the initialize method of model should be called.
After a bit of investigation here is what i found out, the Backbone.Collection function is as follows:
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, {silent: true, parse: options.parse});
};
So if you create an initialize method for your Collection like this
initialize: function() {
console.log('hello collection!');
}
You will notice that the hello collection is logged before the hello from model. So the model initialisation must come from the reset function after the initialize-call. rest won't be called unless you have models passed onto your collection, which you at a quick glance don't seem to be doing, but actually in
var m = new Bar({});
Backbone interprets the {} as a model and thus initializes it in the reset-function. But {} isn't a model you say? Well, Backbone isn't too picky about that, it just needs an array of hashes that could or could not contain model attributes. The reset-function eventually leads to the add-function and finally all roads go to Rome, or should i say the _prepareModel-function
_prepareModel: function(attrs, options) {
if (attrs instanceof Model) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options || (options = {});
options.collection = this;
var model = new this.model(attrs, options);
if (!model._validate(model.attributes, options)) return false;
return model;
}
What happens here is that the Collection checks whether or not it has been passed a model or a hash of attributes and in the hash-of-attributes case it just creates a new model based on its defined model and passes that hash along.
Hope this not only solves the problem, but sheds some additional light on what happened there. And of course I warmly promote for everyone to read up on the backbone source code, the baddest OG of documentation.
I am wondering if there is any way to pass in a value when I instantiate a new Collection, which will be set as a property on all new models that are added to the collection. E.g.,
allSchools = [/* list of schools */];
this.schoolTypes = new Backbone.Collection([], { model:SchoolType }); //pass in allSchools here, somehow
this.schoolTypes.add({name:'New SchoolType'});
where the newly added Model will have a this.allSchools (or this.options.allSchools or something like that). Seems there should be a simple enough way to do this? Currently I'm just accessing a global allSchools object but its not very modular.
It may not be the best way, but you could add a backward link to the model to let it access it's parent collection :
this.schoolType.allSchools = allSchools;
var col = this.schoolType;
this.schoolType.each(function(el,i){
el.collection = col;
});
// ...
// then access all the schools from your SchoolType model `m` :
if(m.collection)
var allSchools = m.collection.allSchools;
As mu mentioned in his comment, models have a built-in .collection property. So, if I set a property on the collection, I can access it from any model within the collection like so:
schoolType = schoolTypes.at(0);
allSchools = schoolType.collection.allSchools;