Problems with passing model to view from local storage - backbone.js

During the initialize function of my app I would like to default to my search page and pass my LeagueCollection as the model.
I am encountering an issue where I can add a watch to this.searchResults in my App initialize and see models: Array[3] as expected,
but when the this.model.toJSON() in the view is called I get the error object has no method toJSON.
This code was working fine with a in memory collection and then I switched to using backbone.localstorage.js to store the app data locally.
So my question is: why is the model not populated in the view?
In my main.js I have
var AppRouter = Backbone.Router.extend({
routes: {
"": "list",
...
},
initialize: function () {
this.searchResults = new LeagueCollection();
this.searchPage = new SearchPage({
model: this.searchResults.fetch()
});
this.searchPage.render();
},
...
});
In my Search Page view
window.SearchPage = Backbone.View.extend({
initialize:function () {
this.template = _.template(tpl.get('search-page'));
},
render:function (eventName) {
var self = this;
$(this.el).html(this.template(this.model.toJSON()));
this.listView = new LeagueListView({el: $('ul', this.el), model: this.model});
this.listView.render();
return this;
},
...
});

The method collection.fetch doesn't return the collection -- it's asynchronous. What you probably want is to use its success callback:
this.searchResults = new LeagueCollection();
var self = this;
this.searchResults.fetch({
success: function(collection, response) {
self.searchPage = new SearchPage( { model: collection } );
self.searchPage.render();
}
});

Related

backbone + requirejs: scope issue

I have 2 views, one is a list of "timetracks" and the other is a form to create a timetrack/s
The first one has a collection attached.
The second one, the timetraks form, it defines a "create" function that makes reference to the first one to rerender timetraks view once a new timetrack is created.
timetracks code:
define(['backbone','collections/timetracks', 'views/timetracks/item'], function(Backbone, instanceTimeTracksCollection, TimeTrackView){
var TimeTrackGrid = Backbone.View.extend({
//......
});
return TimeTrackGrid;
});
The form code:
define(['backbone', 'collections/timetracks'], function(Backbone, instanceTimeTracksCollection){
//...............
//next comes my issue:
create: function(){
instanceTimeTracksCollection.create(indexed_array,{
success: function(model, response) {
console.info('model created, response = ',response);
// timeTracksGrid is out of scope, timeTracksGrid would be an instance of timetracks.
timeTracksGrid.render();
},
error: function(error){
console.info('error=',error);
},
wait:true
});
}
});
... and finally I have app.js where the instances of both views are defined:
requirejs(['backbone','views/timetracks/new','views/timetracks/list'],
function(Backbone, newTimeTrackForm, timeTracksGrid) {
var grid = new timeTracksGrid();
var formView = new newTimeTrackForm();
});
How could I render the timetracks view once a new timetrack is created?
**************************** UPDATE *************************************
This is my new version of the code. The issue now is that "this.listenTo(this.collection, "add", this.render);" is overlapping with "this.collection.fetch". As a result the timetracks records are rendered multiple times.
// timetracks view
define(['backbone','collections/timetracks', 'views/timetracks/item'], function(Backbone, timeTracksCollection, TimeTrackView){
var TimeTrackGrid = Backbone.View.extend({
//....
initialize: function(){
_.bindAll(this, 'render', 'generateTimeTracks', 'appendTimeTrack');
this.listenTo(this.collection, "add", this.render);
this.render();
}
render: function(){
$(this.el).html("<table border='1'></table>");
this.collection.fetch({
success: this.generateTimeTracks
});
},
generateTimeTracks : function(){
var self = this;
_(this.collection.models).each(function(item){
self.appendTimeTrack(item);
}, this);
},
appendTimeTrack: function(item){
var timeTrackView = new TimeTrackView({
model: item
});
$('table', this.el).append(timeTrackView.render().el);
}
}
Some other changes:
on app.js instead doing {model:myCollection} as you suggested I'm doing {collection: myCollection}
my form code creates a new model by calling this.collection.create
Thanks again !
A different solution would be to create the views and your collection seperately.
Then in your app.js you could pass the collection to both views. In the initialize function of the TimeTrackGrid you should listen to the "add" event of models on the collections. When such an event is fired you should render the view.
In the create method of your form view you could add the data to your collection. This way your views don't have to know anything about each other which better conforms the Model and View separation.
Thus:
//the grid view
define(['backbone', 'collections/timetracks', 'views/timetracks/item'], function (Backbone, instanceTimeTracksCollection, TimeTrackView) {
var TimeTrackGrid = Backbone.View.extend({
initialize: function () {
//start listening to models being added
this.listenTo(instanceTimeTracksCollection, "add", this.render)
},
render: function () {
//render your view
return this;
}
});
return TimeTrackGrid;
});
//and the form view
define(['backbone', 'collections/timetracks'], function (Backbone, instanceTimeTracksCollection) {
//...............
//next comes my issue:
create: function () {
var data = //get the data from the form
instanceTimeTracksCollection.add(data) //if you have defined a model on your collection, backbone will automatically instantiate the model
}
});
//and you app -> notice the reference to the collection definition
requirejs(['backbone','views/timetracks/new','views/timetracks/list', 'collections/timetrackcollection'],
function(Backbone, newTimeTrackForm, timeTracksGrid) {
var instanceTimeTracksCollection = new TimeTracksCollection();
var grid = new timeTracksGrid({model : instanceTimeTracksCollection});
var formView = new newTimeTrackForm(model : instanceTimeTracksCollection);
});
EDIT=========================================================
fetch the config here
requirejs(['backbone','views/timetracks/new','views/timetracks/list'],
function(Backbone, newTimeTrackForm, timeTracksGrid) {
var grid = new timeTracksGrid();
var formView = new newTimeTrackForm();
var collection = new Collection();
collection.fetch()
});
change your view to:
define(['backbone','collections/timetracks', 'views/timetracks/item'], function(Backbone, timeTracksCollection, TimeTrackView){
var TimeTrackGrid = Backbone.View.extend({
//....
initialize: function(){
_.bindAll(this, 'render', 'generateTimeTracks', 'appendTimeTrack');
// maybe backbone does not fire the add event after fetch
// I believe it does, but I might be mistaken. You will have to look that up
this.listenTo(this.collection, "add", this.render);
this.render();
}
//model is passed to the render method by backbone
render: function(model){
$(this.el).html("<table border='1'></table>");
$('table', this.el).append(new TimeTrackView({model : model}).render().el);
},
//unused now
generateTimeTracks : function(){
var self = this;
// backbone has underscore build in
// so use this instead
this.collection.each(function(item){
//do something with item
}
_(this.collection.models).each(function(item){
self.appendTimeTrack(item);
}, this);
},
//unused now
appendTimeTrack: function(item){
var timeTrackView = new TimeTrackView({
model: item
});
$('table', this.el).append(timeTrackView.render().el);
}
}

Backbone, collection models can't be accessed in function that fires from router Vent

So my issue is that the collection in my function that is being fired from my router vent/event aggregator does not have access to my main collection's fetched models.
My guess is that it's an asynchronous call issue, but how can I make it so the vented function call WAITS until the collection/models are fetched before executing? Or is that even my issue?
Here's is my relevant code. I'm using require.js and backbone to create a modular AMD app. Thank you so much in advance:
main.js
require(['views/app'], function (AppView) {
window.App = {
Vent : _.extend({}, Backbone.Events)
};
new AppView();
router.js
define([
'backbone',
], function(Backbone){
var MainRouter = Backbone.Router.extend({
routes: {
'levelone/:id':'showWork'
},
showWork: function (index){
App.Vent.trigger('addressChange', {
index: index
});
}
});
return MainRouter;
});
App.js
define([
'backbone',
'views/levelone/LevelOneView',
'views/leveltwo/LevelTwoView',
'views/static/StaticView',
'router'
],
function(Backbone, LevelOneView, LevelTwoView, StaticView, MainRouter){
var AppView = Backbone.View.extend({
el: $("body"),
events: {
...
},
initialize: function(){
new LevelOneView();
App.router = new MainRouter();
Backbone.history.start();
},
.............
LevelOneView.js
initialize:function() {
this.getCollection();
this.domSetup();
App.Vent.on('addressChange', this.addressChange, this);
},
getCollection : function(){
var self = this;
onDataHandler = function(collection) {
self.LevelTwoCollectionGrab();
};
this.collection = new LevelOneCollection([]);
this.collection.fetch({ success : onDataHandler, dataType: "jsonp" });
},
// We grab a Level Two Collection here so we can take the ids from it and add them to our Level One collection.
// This is necessary so we can create links between the two levels.
LevelTwoCollectionGrab: function(){
var self = this;
this.leveltwocollection = new LevelTwoCollectionBase([]);
onDataHandler = function(collection){
self.render();
self.$el.animate({
'opacity': 1
}, 1200);
self.renderLevelTwoIds();
self.setLevelTwoids();
self.attachLevelTwoLink();
}
this.leveltwocollection.fetch({success : onDataHandler, dataType: "jsonp"});
},
renderLevelTwoIds: function(){
return this;
},
render: function(){
var pathname = window.location.hash;
this.setModelId(this.collection.models);
this.addPositionsToIndex();
this.determineModels();
this.attachLevelTwoLink();
.......
},
addressChange: function(opts){
console.log(this.collection.models)
//returns a big fat empty array. WHY?!
}
You could use the jQuery Promises returned by fetch to help you know when both collections are fetched.
initialize:function() {
this.getCollection();
this.domSetup();
App.Vent.on('addressChange', this.addressChange, this);
},
getCollection : function(){
var self = this;
console.log('should be first');
this.collection = new LevelOneCollection([]);
this.fetchingLevelOne = this.collection.fetch({ dataType: "jsonp" });
this.fetchingLevelTwo = this.leveltwocollection.fetch({ dataType: "jsonp"});
// wait for both collections to be done fetching.
// this one will always be called before the one in addressChange
$.when(this.fetchingCollectionOne, this.fetchingCollectionTwo).done(function(){
console.log('should be second');
self.render();
self.$el.animate({
'opacity': 1
}, 1200);
self.renderLevelTwoIds();
self.setLevelTwoids();
self.attachLevelTwoLink();
});
},
renderLevelTwoIds: function(){
return this;
},
render: function(){
var pathname = window.location.hash;
this.setModelId(this.collection.models);
this.addPositionsToIndex();
this.determineModels();
this.attachLevelTwoLink();
.......
},
addressChange: function(opts){
var self = this;
// wait for both collections to be done fetching.
// this one will always be called AFTER the one in getCollection
$.when(this.fetchingCollectionOne, this.fetchingCollectionTwo).done(function(){
console.log('should be third');
console.log(self.collection.models);
});
}
A nice thing about this, if the user is very very fast at typing in the address bar, and several addressChange calls are made, they will all wait until the collections are fetched and will execute in the proper order.
I think I solved it. Basically, I'm now calling the function inside of $.when function--
Like so:
$.when(this.collection.fetch(), this.leveltwocollection.fetch()).done(function(){
$.when(self.render()).done(function(){
_.each(self.collection.models, function(model){
var wpid = model.get('id'),
bbid = model.id;
if (wpid == index){
window.App.InfoPos.pos5 = bbid;
var modelinfo = model.toJSON();
$('.box5').empty();
$('.box5').html(tmplOne(modelinfo));
self.$el.animate({
'opacity': 1
}, 1200);
}
});
});
});
The function launches from inside the when call and then waits until completed before executing anything in the done function. Works now! Thanks for the help all, especially you Paul.

View with .fetch() not rendering

When I render this view
var MyView = Backbone.View.extend({
/* el : '.myview', used when rendered on router request */
render : function () {
var data = new Data(); /* a collection from ajax request */
var that = this;
data.fetch({
success : function (bla, data) {
var template = _.template( $('#temp').html(), {data: data.players} );
that.$el.html(template);
}
});
}
});
on router request, it works:
var Router = Backbone.Router.extend({
routes : {
'bla' : 'bla'
}
});
var myView = new MyView();
var router = new Router();
router.on('route:bla', function () {
myView.render();
});
But when I want it simply to load with the page, it doesn't:
var myView = new MyView({ el: $(".myview") });
Because in the last line you posted, you missing call to render function.
var myView = new MyView({ el: $(".myview") });
myView.render();
Besides of that, I see the workflow of initialization a bit wiered. Namely, requesting the data inside .render() function, that violates view single responsibility. Something, that I do in my applications.
var Router = Backbone.Router.extend({
routes : {
'bla' : 'bla'
},
bla: function () {
app.BlaApp();
}
});
app.BlaApp = function () {
var collection = new Collection();
collection.fetch({
success: function (collection) {
var view = new app.BlaView({model: collection});
view.render();
}
})
}
app.BlaView = Backbone.View.extend({
render: function () {
var template = // render template based on this.model;
this.$el.html(template);
return this;
}
});

backbone model variable only populating when I step into method

The situation I'm experiencing is that my fetch is working fine, I can see the data in my backbone collection, if I step into the method where I assign the item from the collection to the model var or pause for a second or two, all is well: the line of code this.member gets populated
this.member = this.members.get(1);
. If I just let the code run, I wind up passing in a null model to my view. I don't understand what I'm missing. Do I need to bind data in the collection before I access the collection? I'm trying to fall in love with Backbone, but so far, she's been a cruel mistress...
`
//create the namespace
var Endeavor = {
Models: {},
Collections: {},
Views: {},
Templates: {}
};
Endeavor.Models.Member = Backbone.Model.extend({
idAttribute: "Id"
});
Endeavor.Collections.Members = Backbone.Collection.extend({
model: Endeavor.Models.Member,
url: "Http://localhost:60000/api/members/" + "1", // $.cookie('UserId')
initialize: function () {
console.log("Members collections init");
}
});
Endeavor.Views.MemberView = Backbone.View.extend({
id: "memberForm",
template: "#memberTemplate",
initialize: function () {
console.log('init member view');
},
render: function () {
console.log('memberView render called');
console.log(this.model.toJSON());
var html = $(this.template).tmpl();
$(this.el).html(html);
$("#Name").text = this.model.Name;
}
});
jQuery(document).ready(function () {
// router
Endeavor.Router = Backbone.Router.extend({
routes: {
"": "lists",
},
lists: function () {
this.members = new Endeavor.Collections.Members();
this.members.fetch();
this.member = this.members.get(1);
var memberView = new Endeavor.Views.MemberView({ model: this.member });
memberView.render();
$("#content").html(memberView.el);
}
});
// populate local variables
var appRouter = new Endeavor.Router();
Backbone.history.start();
});
`.
Members.fetch is async call. Try to render view on members reset event, or pass success callback in fetch method, or call this.members.fetch({async: false}). There are many different options.

How to insert results of fetch of model in a view

I've fetch() a model by a server and I want to render the results of fetch() with a view.
The results of fetch() is an array of objects (var risultati) and I want render this var risultati. I've tried but nothing works.
var AppRouter = Backbone.Router.extend({
routes: {
"": "list",
},
initialize: function () {},
list: function () {
var utente = new Person();
var risultati;
utente.fetch({
success: function (data) {
var ris = data.attributes;
var risultati = ris.results;
console.log(risultati); /* risultati contains array of object to render*/
}
});
this.page = new UserListView({
model: this.utente
});
$('body').append(this.page.$el);
}
});
You may be having problems because your call to render the view is occurring separate from your utente.fetch() call.
Since .fetch() is asynchronous, your view code will be executed before .fetch() has finished. You should add the view creation/rendering as part of the success function, or you should bind the change event that occurs when the model is updated to fire off a new function that contains your view creation.
You should separate your MVC logic... don't attach objects (collections I think) to a router route handler.
Assuming that you are trying to render a collection of person models, I suggest you use a model and view for the person and a collection and a view for handling the "array of objects" :
var Person = Backbone.Model.extend({
initialize : function(){
// initialize the view
this.view = new PersonView({model : this});
}
}),
PersonView = Backbonke.View.extend({
render : function(){
// render your person
}
}),
UserList = Backbone.Collection.extend({
model : Person,
initialize : function(){
this.view = new UserListView({
collection : this
});
},
update : function(){
var self = this;
this.fetch({
success: function (data) {
var ris = data.attributes;
var risultati = ris.results;
console.log(risultati); /* risultati contains array of object to render*/ self.view.render();
}
});
}
}),
UserListView = Backbone.View.extend({
render : function(){
this.collection.each(function(el,i){
el.view.render();
});
}
});
and then use it as :
var page = new UserList();
var AppRouter = Backbone.Router.extend({
routes: {
"": "list",
},
initialize: function () {},
list: function () {
page.update();
}
});
Hope this helps!

Resources