Backbone.js bug? - javascript

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.

Related

The collection nested inside firebase collection's model doesn't have add function

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

Does not Backbone fetch call constructors for retrieved models?

It seems that neither Backbone collection fetch nor parse functions call constructor for the retrieved models; am I right? So if one needs to initialize some special properties for them (in constructor or initializer), he must call it explicitly. E. g., in success callback of the fetch function. Or may be I miss something?
Example follows.
return t.collection.fetch({
success: function () {
for (var i = 0; i < t.collection.models.length; i++) {
t.collection.models[i] = new MyModel(t.collection.models[i]);
}
},
});
Here the t.collection is of type
class MyModelsCollection extends Backbone.Collection<MyModel> {
url = "/api/MyModels/";
}
The TypeScript has been used.
Solution. The answer made me to check the TypeScript syntax used here. I have found that it was incorrect. It must be like in this answer (except that the super() call should go first). So one should set the model property of the collection manually in the constructor.
May be it was obvious for everyone else.
If you just link your model class to model option of the collection, everything will work by itself.
var MyModel = Backbone.Model.extend({});
var MyCollection = Backbone.Colleciton.extend({
url: "/api/MyModels/",
model: MyModel
});
When instance of MyCollection will be fetched and there will be an array of models from backend, it will create needed amount of MyModel instances.

Having trouble moving a existing Model file into Require.js

I have moved the project into require now I am attempting to re-write some the code using backbone and underscore. The traditional model, view , controller files exist. Below Is the start of an attempt to move the Model into a require define.
define ("Model", function () {
var Model = function (connectToServer, soundTypeNum, isMobile)
{
//code
};
return Model;
// };//Error
});//Fixed
But I am getting the following error:
Uncaught SyntaxError: Unexpected end of input
EDIT: Now I am Getting Model is undefined when controller tries to create a new instance of Model:
var model = new Model(connectToServer);
Any ideas?
I believe require.js must take an array of dependencies, even if there's only one. Also make sure you're passing the dependencies to the function, as Nit said.
define(["Model"], function(theModel) {
var Model = // initialize Model here
return Model;
});
If you're not initiating Model somewhere else, then why is it a dependency? You can pass a blank array of dependencies to require's function, if need be:
define([], function() {
var Model = // initialize Model here
return Model;
});
I'm not sure how you're planning on bringing backbone into play later "to make code more manageable". I would think it would be easier to declare Model as a backbone model in this file where you initiate it. Just define backbone as a dependency and extend its Model class.
define(["underscore", "backbone"], function(_, Backbone) {
var Model = Backbone.Model.extend({
// initialize Model here.
});
return Model;
});
Hope this helps.

In Backbone, can I add model behavior to a collection?

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

Backbone Model: Keep Collection when saving

I have a simple Backbone model that looks like this:
(function () {
App.Company = Backbone.Model.extend({
defaults: {},
urlRoot: "/Contacts/Companies",
initialize: function () {
var contactPersons = this.get("ContactPersons") || [];
this.set("ContactPersons", new App.ContactPersonCollection(contactPersons));
}
});
})();
Whenever I save the model to the server, the ContactPersons collection is reset to an Array.
Is it really necessary for me to manually turn it into a collection, after a model is saved?
UPDATE: This works as intended -- See answer for better approach (IMHO)
(function () {
App.Company = Backbone.Model.extend({
defaults: {},
urlRoot: "/Contacts/Companies",
initialize: function () {
var contactPersons = this.get("ContactPersons") || [];
if (_.isArray(contactPersons)) {
this.set("ContactPersons", new App.ContactPersonCollection(contactPersons));
}
},
parse: function (response) {
if (response.ContactPersons && _.isArray(response.ContactPersons)) {
response.ContactPersons = new App.ContactPersonCollection(response.ContactPersons);
}
return response;
}
});
})();
When you send data back from the server, how are you handling the response? For example if you just send back a [{},{},{}] I don't think Backbone automatically knows to treat that as a collection. Thus, it sets the ContactPersons attribute as what it gets, your vanilla array.
What you can do, is override your set function inside your model which will take the array of objects passed in and write to the collection as proper. See this example:
set: function(attributes, options) {
if (_.has(attributes, 'ContactPersons') && this.get("ContactPersons")) {
this.get('ContactPersons').reset(attributes.ContactPersons);
delete attributes.ContactPersons;
}
return Backbone.Model.prototype.set.call(this, attributes, options);
}
So basically as long as your server response is properly namespaced (response.ContactPersons) then after parsing it will pass your response to the set function. The collection data is treated specially as a collection. Here, I'm just reseting the collection that already exists with the new data. All your other model attributes should continue to be passed on to the original set().
UPDATE - Growing doubt about own answer
I haven't been able to get this question/answer out of my mind. It certainly works, but I'm becoming unconvinced that using a modified set() vs. just doing things in parse() is any better. If someone has some comments on the difference between using a modified set() vs. parse() with nested models, I'd really welcome the input.

Categories