Disappearing models in Backbone collection - backbone.js

I've got a Backbone model line that contains a collection of the model Stop.
At some point, I want to iterate through the stops in the line and get the total travel time along the line, using the Underscore function reduce.
This does not work, however. It seems that something happens with the collection at some point.
It seems to contain only one object without meaningful attributes, although I know for a fact that it has been populated with four stop-models with valid attributes.
The model:
App.models.Line = Backbone.Model.extend({
initialize: function() {
var stops = new App.models.Stops({
model: App.models.Stop,
id: this.get("id")
});
stops.fetch();
this.set({
stops: stops
});
this.set({unique: true});
this.calculateTotalTime();
},
calculateTotalTime: function() {
this.get("stops").each(function(num) {
console.log("Model!");
});
console.log("Unique: ", this.get("unique"));
}
});
Console printout is:
Model!
Unique: true
There should be four "Model!", since the number of models is four.
The strangest thing is that everything works just fine in the console:
window.line.get("stops").each(function(num) {
console.log("Model!");
});
Model!
Model!
Model!
Model!
The JS is compiled with Sprockets:
//= require ./init
//= require ./lib/jquery
//= require ./lib/underscore
//= require ./lib/backbone
//= require ./lib/raphael
//= require_tree ./controllers
//= require_tree ./models
//= require_tree ./views
//= require ./main
init.js:
window.App = {};
App.views = [];
App.models = [];
main.js:
$(function() {
window.line = new App.models.Line({name: "4", id: 4});
window.lineView = new App.views.Line({model: line});
$("#outer").append(lineView.render().el);
});
Some other strange behaviour:
console.log(this.get("stops")) in the model yields this fairly normal object:
child
_byCid: Object
_byId: Object
_onModelEvent: function () { [native code] }
_removeReference: function () { [native code] }
id: 4
length: 4
models: Array[4]
0: Backbone.Model
1: Backbone.Model
2: Backbone.Model
3: Backbone.Model
length: 4
__proto__: Array[0]
__proto__: ctor
But calling console.log(this.get("stops").models), which should yield the array, returns only this, an array of a single object with no useful attributes:
[
Backbone.Model
_callbacks: Object
_changed: false
_changing: false
_escapedAttributes: Object
_previousAttributes: Object
attributes: Object
id: 4
model: function (){ return parent.apply(this, arguments); }
__proto__: Object
cid: "c1"
id: 4
__proto__: Object
]
I suspect this is all down to some misunderstanding about the nature of this. Glad for any help provided.

stops.fetch() is an asynchronous process, so the code that you have written right after it will likely fire before the results of the fetch have come back from the server.
you'll need to modify your code to run everything after the fetch comes back. the easiest way to do this is with the reset event from the stops collection:
App.models.Line = Backbone.Model.extend({
initialize: function() {
var stops = new App.models.Stops({
model: App.models.Stop,
id: this.get("id")
});
// store the stops, and then bind to the event
this.set({stops: stops});
stops.bind("reset", this.stopsLoaded, this);
stops.fetch();
},
stopsLoaded: function(){
// get the stops, now that the collection has been populated
var stops = this.get("stops");
this.set({unique: true});
this.calculateTotalTime();
},
calculateTotalTime: function() {
this.get("stops").each(function(num) {
console.log("Model!");
});
console.log("Unique: ", this.get("unique"));
}
});
the reason it works in your console is because by the time you type out the code to evaluate the model's stops, the fetch call has already returned and populated the collection.
hope that helps

Related

Backbone error: Model is not a constructor

Afternoon all, I'm relatively new to backbone and have been stumped for 3 days with this error which I have not seen before.
I have a collection 'TestCollection' which defines it's model as a function. When the collection is loaded I get an error the first time it attempts to make a model with class 'TestModel'.
The error I get is:
Uncaught TypeError: TestModel is not a constructor
at new model (testCollection.js:14)
at child._prepareModel (backbone.js:913)
at child.set (backbone.js:700)
at child.add (backbone.js:632)
at child.reset (backbone.js:764)
at Object.options.success (backbone.js:860)
at fire (jquery.js:3143)
at Object.fireWith [as resolveWith] (jquery.js:3255)
at done (jquery.js:9309)
at XMLHttpRequest.callback (jquery.js:9713)
I believe I have given both the collection and the model all of the code they should need to work. It feels like something has gone wrong with the loading, but when I put a console.log at the top of the model file I could see that it is definitely being loaded before the collection attempts to use it.
Any help would be massively appreciated.
TestCollection:
define([
'backbone',
'models/testModel'
], function(Backbone, TestModel) {
var TestCollection = Backbone.Collection.extend({
model: function(attrs) {
switch (attrs._type) {
case 'test':
console.log('making a test model')
return new TestModel();
}
},
initialize : function(models, options){
this.url = options.url;
this._type = options._type;
this.fetch({reset:true});
}
});
return TestCollection;
});
TestModel:
require([
'./testParentModel'
], function(TestParentModel) {
var TestModel = TestParentModel.extend({
urlRoot: 'root/url',
initialize: function() {
console.log('making test model')
}
});
return TestModel;
});
File where TestCollection is made:
define(function(require) {
var MyProjectCollection = require('collections/myProjectCollection');
var TestCollection = require('collections/testCollection');
Origin.on('router:dashboard', function(location, subLocation, action) {
Origin.on('dashboard:loaded', function (options) {
switch (options.type) {
case 'all':
var myProjectCollection = new MyProjectCollection;
myProjectCollection.fetch({
success: function() {
myProjectCollection.each(function(project) {
this.project[project.id] = {};
this.project[project.id].testObjects = new TestCollection([], {
url: 'url/' + project.id,
_type: 'test'
});
});
}
});
}
});
});
I've had a look around stack overflow, it does not appear to be the issue below (which seems to be the most common issue).
Model is not a constructor-Backbone
I also do not think I have any circular dependencies.
Any help would be massively appreciated as I am completely stumped. I've tried to include only the relevant code, please let me know if additional code would be useful.
Thanks
I can't say for other parts of the code but an obvious problem you have is misunderstanding what data is passed to the model creator function.
var TestCollection = Backbone.Collection.extend({
model: function(attrs) {
switch (attrs._type) { // attrs._type does exist here
console.log( attrs ); // Will print { foo: 'bar' }
case 'test': // This will always be false since attrs._type does not exist
console.log('making a test model')
return new TestModel();
default:
return new Backbone.Model(); // Or return some other model instance,
// you MUST have this function return
// some kind of a Backbone.Model
}
},
initialize : function(models, options){
this.url = options.url;
this._type = options._type;
this.fetch({reset:true});
}
});
new TestCollection([ { foo: 'bar' }], {
url: 'url/' + project.id,
_type: 'test' // This will NOT be passed to the model attrs, these are
// options used for creating the Collection instance.
})
To re-iterate. When you instantiate a Collection you pass an array of plain objects [{ foo: 'bar'}, { foo: 'baz'}] ( or you get them via fetch like you're doing ). That object will be passed as the attrs parameter in the model function, and the model function MUST return at least some kind of a Backbone.Model instance so you need a fallback for your switch statement.

Backbone "at()" is returning undefined as result in Firebug

I'm trying to follow a screencast on how to return a result from a database using Backbone.js and REST. My RESTful service (at least I think it's RESTful -- REST is new to me) is serving up JSON data like you would want. Everything appears to work fine until the last step when I try to access the data using at(). Here is my code.
This is my Backbone:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html());
};
var vent = _.extend({}, Backbone.Events);
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'*other' : 'other'
},
index: function() {
},
other: function() {
}
});
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
url: '../leads/main_contact'
});
new App.Router;
Backbone.history.start();
})();
In the Firebug console, which is what's used in Jeffrey Way's screencast, I type the following:
mains = new App.Collections.Mains();
mains.fetch();
mains.toJSON();
That works fine. I can use Firebug to see that there is the proper data there. Here is my result:
[Object { id="1023", timestamp="2012-05-16 08:09:30", FName="Eulàlia", more...},...
But when I try to access the object with the id of 1023, I get "undefined." I do this:
mains.at(1023).get('FName');
What am I doing wrong?
the at method retrieves an element at a specific index in the collection.
So if you want to get the element at position 1023, you need 1023 items in your collection. Which you probally don't have.
The id that you have and set to 1023 has nothing to do with index in that collection.

Empty backbone collection/model at working API?

i try to fetch a record of a rails-api (same host) into my backbone collection. i have the following code:
// Models
App.GeeksModel = Backbone.Model.extend({
urlRoot: "/geeks",
idAttribute: "id"
});
// Collections
App.GeeksCollection = Backbone.Collection.extend({
url: "/geeks",
model: App.GeeksModel
});
in my router i have the following
// Router
App.GeekRouter = Backbone.Router.extend({
routes: {
"": "index"
},
initialize: function() {
console.log("router - init");
},
index: function() {
console.log("route - index");
var geekCollection = new App.GeeksCollection();
var mapView = new App.GeeksMapView({ el: $("#foo"), model: geekCollection });
geekCollection.fetch();
}
});
when browsing the url, the view loads correctly and at the server i see, that one entry is fetched from the database. but as soon as i check the model length in my view using
this.model.length
the collection is empty... any advice on this?
thanks
EDIT 1:
when changing the index router method to
var mapView = new App.GeeksMapView({ el: $("#map"), collection: geekCollection });
and e.g. check for the collection length in the views intialize method
...
initialize: function() {
this.render();
console.log(this.collection.length);
},
...
it retunes 0 as well... so nothing changed!
I believe you want to do collection.length or if accessing from the model - each model holds reference to collection in which it was created model.collection.length - if this is referencing to collection doing just this.length should be enough, if it's a model then this.collection.length will do it for you.
Models have no property length so should always be undefined unless you define it yourself.

Can't display data using Handlebars template and BackboneJS

I have been trying to display some data(a json object with only three properties) by fetching it from server (2 lines of php code). To fetch and display that data in html page I've used BackboneJS and Handlebars template respectively. Here is the javascript code
var User = Backbone.Model.extend({
urlRoot:"getUser/"
});
var UserView = Backbone.View.extend({
el:$("#user"),
initialize: function(){
this.model.bind("change", this.render());
},
render: function(){
var templateSource = $("#user-temp").html();
var template = Handlebars.compile(templateSource);
$(this.el).html(template(this.model));
var newDate = new Date();
console.log("in UserView render :: " + newDate.getTime());
console.log(this.model.toJSON());
//var pp = "nothing";
}
});
var UserRouter = Backbone.Router.extend({
routes:{
"":"userDetails"
},
userDetails:function(){
//var newUser = new User({id:1});
var newUser = new User();
var userView = new UserView({model:newUser});
var newDate = new Date();
newUser.fetch({
success:function(){
console.log("in router :: " + newDate.getTime());
console.log(userView.model.toJSON());
}
});
}
});
Handlebars template in index.html page
<div id="user"></div>
<script id="user-temp" type="text/x-handlebars-template">
<div>
ID {{attributes.id}}
Name {{attributes.name}}
Age {{attributes.age}}
</div>
</script>
PHP code
$user = array("name"=>"Arif","id"=>"1","age"=>"100");
echo json_encode($user);
Now the problem is I can't see the data ($user) i'm sending from server in index.html page, in console (google chrome) i've rather found this
in UserView render() :: 1350880026092
Object
__proto__: Object
in router :: 1350880026097
Object
age: "100"
id: "1"
name: "Arif"
__proto__: Object
(The BIG numbers in console is time in milliseconds.)
But If I change the code for console output (just showing the model)
(in UserView render() function)
console.log(this.model);
(in UserRouter userDetails() function)
console.log(userView.model);
Then the console looks like this
in UserView render :: 1350881027988
child
_changing: false
_escapedAttributes: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object <<======
age: "100"
id: "1"
name: "Arif"
__proto__: Object
changed: Object
cid: "c0"
id: "1"
__proto__: ctor
in router :: 1350881027995
child
_changing: false
_escapedAttributes: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object <<======
age: "100"
id: "1"
name: "Arif"
__proto__: Object
changed: Object
cid: "c0"
id: "1"
__proto__: ctor
Here i can see the attributes (arrow marks <<====== )
So what am i doing wrong? Am i missing some basic concepts here? By the way, I'm new to Handlebars and BackboneJS. Moreover its my first question in stackoverflow, so if you think the info i've given isn't enough, please feel free to ask what further info you need.
Thanks in advance.
You bind your model to this.render() which you means you execute your render function and then bind your model to whatever render returns (nothing, in your case).
Try
initialize: function(){
_.bindAll(this, 'render'); // guarantees the context for render
this.model.bind("change", this.render);
}
or, with a more up to date syntax (see the changelog for 0.9.0 http://backbonejs.org/#changelog, bind and unbind have been renamed to on and off for clarity)
initialize: function(){
_.bindAll(this, 'render');
this.model.on("change", this.render);
}

populating nested collection with nested json

Solution
in my route
Myapp.Routes = Backbone.Router.extend({
init: function(){
user = new User();
user.fetch({user,
success: function(response){
user.classlist = new classes(response.attributes.classes);
});
}
});
I've got a serialized json array being returned from my server, and I am trying to put the nested objects into my nested collections.
This answer, I thought was going to get me there, but I'm missing something.
How to build a Collection/Model from nested JSON with Backbone.js
The json which I am trying to populate my nested model with is
{first_name: "Pete",age: 27, classes: [{class_name: "math", class_code: 42},{class_name: "french", class_code: 18}]}
I create my user model
MyApp.Models.Users = = Backbone.Model.extend({
initialize: function(){
this.classlist = new MyApp.Collections.ClassList();
this.classlist.parent = this;
}
});
I had tried to follow the example on the other page, and use
this.classlist = new MyApp.Collections.ClassList(this.get('classes'));
this.classlist.parent = this;
but this.get('classes') returns undefined.
I've also tried getting the classes array through this.attributes.classes, but that is also undefined.
------------updated to include re-initialize --------------------
The function where I am initializing the user and classes is in the User routes and is called re-initialize. I use this function to fetch the user and their classes and store the object.
re_initialize: function(id){
user = new MyApp.Models.User();
MyApp.editingClasses.url = 'classes/'+id;
MyApp.editingClasses.fetch({
success: function(response){
MyApp.editingClasses.parse(response);
}
});
new MyApp.Views.ClassesInput();
},
As you can see, I'm calling the parse explicitly in the success function, but it isn't adding the classes to the collection.
I can't include the 'collection' because for some reason I can't access it in backbone.
the user model, after getting returned to backbone includes the classes array, which I am trying to put into the ClassList collection.
The user model object copied from the javascript terminal looks like this.
attributes: Object
created_at: "2012-01-05T16:05:19Z"
id: 63
classes: Array[3]
0: Object
created_at: "2012-01-18T20:53:34Z"
id: 295
teacher_id: 63
class_code: 42
updated_at: "2012-01-18T20:53:34Z"
class_name: math
__proto__: Object
1: Object
2: Object
length: 3
__proto__: Array[0]
You can use the parse function to pre-process the server response:
MyApp.Models.Users = Backbone.Model.extend({
parse: function(response) {
var classesJSON = response.classes;
var classesCollection = MyApp.Collections.ClassList(classesJSON);
response.classes = classesCollection;
return response;
}
});
var user = new MyApp.Models.Users();
user.fetch();
// You should now be able to get the classlist with:
user.get('classes');
That said, the approach suggested in the other question should also work. It could be that when your initialize function is called, the model hasn't yet been populated with the data?
For example, if you're doing:
var user = new MyApp.Models.Users();
It won't have any attributes yet to give to the classlist collection. Could that be your problem?
Okay! you can maybe fetch the classes this way :
Model :
window.person = Backbone.Model.extend({
defaults: { }
});
Collection :
window.ClassesCollection = Backbone.Collection.extend({
model: person,
url: "http://your/url/data.json",
parse: function(response){
return response.classes;
}
});
Router :
window.AppRouter = Backbone.Router.extend({
routes: {
"" : "init"
},
init: function(){
this.classesColl = new ClassesCollection();
this.classesColl.fetch();
this.classesView = new ClassesView({collection: this.classesColl});
}
});
View : (for rendering every classes)
window.ClassesView = Backbone.View.extend({
el: $('...'),
template: _.template($("...").html()),
initialize: function() {
this.collection.bind("reset", this.render, this);
},
render: function(collection) {
_.each( collection.models, function(obj){
...
//obj.get('class_name') or obj.get('class_code')
...
}, this );
...
return this;
}
});

Resources