loading multiple views in init function breaks one or the other - backbone.js

I'm building my first backbone.js app, and I've run into a problem when trying to initialize my app and display both recipes and a shopping list, both of which are different (yet related) backbone objects.
My init function is
var MyApp= {
Models: {},
Views: {},
Routers: {},
Collections: {},
AppView: {},
Init: function() {
new MyApp.Views.ShoppingList;
new MyApp.Routers.Recipes;
Backbone.history.start();
}
};
Strangely, when I use
new MyApp.Routers.ShoppingList;
new MyApp.Routers.Recipes;
I don't get the shopping list View, I only get the recipes.
I also don't get any errors.
The shopping list router is fairly basic
MyApp.Routers.ShoppingList = Backbone.Router.extend({
routes: {
"": "index",
"shopping_list/:id": "show"
},
index: function(){
console.log('this');
new MyApp.Views.ShoppingList();
}
});
so from what I understand, the app should load the router, and display the view, but I'm not getting that or the console.log.
--------------as requested, here is my 'recipes router'---------------
MyApp.Routers.Recipes = Backbone.Router.extend({
routes: {
"": "index",
"recipes/:id": "show"
},
index: function(){
if(!MyApp.RecipeList){
MyApp.RecipeList = new MyApp.Collections.RecipeList;
MyApp.RecipeList.page = 1;
} else {
MyApp.RecipeList.page++;
}
MyApp.RecipeList.url='/recipes?page='+MyApp.RecipeList.page;
MyApp.RecipeList.fetch({
add: true,
success: function() {
new MyApp.Views.RecipeList({ collection: MyApp.RecipeList});
},
error: function() {
new Error({ message: "Error loading documents." });
}
});
},
show: function(id){
var recipe = MyApp.RecipeList.get(id);
new MyApp.Views.RecipeView({ model: recipe});
},
newRecipe: function(){
new App.Views.Edit({ model: new Recipe() });
},
edit: function(id){
var recipe = new Recipe({ id: id});
recipe.fetch({
success: function(model, resp){
new App.Views.Edit({ model: recipe});
},
error: function(){
new Error({message: "Hey!? Were'd it go? sorry I can't find your recipe"});
window.location.hash = '#';
}
});
}
});
----------------- some progress -----------------------------
I may be wrong, but in commenting out sections of the router, I find that the problem may be caused by my 'routes' as they both have index where the url is empty. Commenting out the 'routes' in one controller/router causes the other controller/router to display.
I've changed the routes so that they are more representative of their namespace
routes{
"recipes" : "recipes"
},
recipes: function()...
but I'm still not getting the right information to display. I'm now trying to figure out if I need an initialize function and what that would look like, or if I've even debugged this properly
--------------------- update, I was using backbone wrong ------------------------
It turns out I believe that I was mis-understanding Routers and was thinking of them more like controllers, so I was calling multiple routers on load, but the page was only loading the last one which pointed to an empty route as you can only request a single url route at a time.
Now I'm loading multiple Views on load and only one router.

After instantiating your view, you still need to render it and add it to the DOM.
index: function(){
console.log('this');
var view = new MyApp.Views.ShoppingList();
//you don't have to append to the whole body, but this is just an example
$('body').append(view.render().el);
}

Related

Cannot access all backbone model attributes from template

The problem I am trying to debug is that, in my backbone view, all the model attributes (name, id, email etc) are available, (I have console.log() the model attributes to verify this), however, when I render the view passing the model, only the name attribute is available in the template, all other attributes are undefined,please is there any thing I am missing, as this is my first backbone application and I have spent hours trying to debug this and have gone through many tutors online and my code seems correct, thanks
//BACKbone view method(I have verified and all the model values are available here)
var profileView=Backbone.View.extend({
el:$('#content'),
initialize:function(){
this.model.bind('change',this.render,this);
},
render:function(){
var self=this;
this.$el.html(_.template(profileTemplate,this.model.toJSON()));
}
});
//HTML TEMPLATE (profileTemplate), only the name attribute is available in the template, //browser gives an error of 'undefined' for all other attributes except the name attribute
<h1><%=name.first%> <%=name.last%> </h1>//displays correctly
<%=email%> //undefined
//SCHEMA
var AccountSchema=new mongoose.Schema({
email:{type:String,unique:true},
password:{type:String},
name:{
first:{type:String},
last:{type:String},
full:{type:String}
},
});
//IT IS SOLVED NOW, IT happened because I executed the fetch command on the model after calling the render method instead of before calling the render method
This is my router, I call the model.fetch() from the router after the view is created. The problem stopped when I called model.fetch() before the rendering of the before
define([''views/profile''],function(,ProfileView){
var router = Backbone.Router.extend({
currentView: null,
socketEvents: _.extend({}, Backbone.Events),
routes: {
'addcontact': 'addcontact',
'index': 'index',
'login': 'login',
'register': 'register',
'forgotpassword': 'forgotpassword',
'profile/:id': 'profile',
'contacts/:id': 'contacts',
"upload/:id":"upload"
},
profile: function(id) {
var model = new Account({id:id});
console.log('profile user:'+id);
this.changeView(new ProfileView({model:model})); //View is created here
model.fetch(); // model is fetched after view is rendered, cause for the problem in my case
}
});
return new router();
});

Roles of backbone views, model, and router

I am developing a backbone application which is using require.js.
I want a user to enter in the 'id' for a model and then either be redirected to a view for that model if it exists, or display an error message if it does not. This sounds extremely simple, but I am having trouble figuring out the roles of each component.
In the application below, the user will come to an index page with an input (with id 'modelId') and a button (with class attribute 'lookup').
The following piece of code is the router.
define(['views/index', 'views/myModelView', 'models/myModel'],
function(IndexView, MyModelView, myModel) {
var MyRouter = Backbone.Router.extend({
currentView: null,
routes: {
"index": "index",
"view/:id": "view"
},
changeView: function(view) {
if(null != this.currentView) {
this.currentView.undelegateEvents();
}
this.currentView = view;
this.currentView.render();
},
index: function() {
this.changeView(new IndexView());
},
view: function(id) {
//OBTAIN MODEL HERE?
//var model
roter.changeView(new MyModelView(model))
}
});
return new MyRouter();
});
The following piece of code is the index view
define(['text!templates/index.html', 'models/myModel'],
function( indexTemplate, MyModel) {
var indexView = Backbone.View.extend({
el: $('#content'),
events: {
"click .lookup": "lookup"
},
render: function() {
this.$el.html(indexTemplate);
$("#error").hide();
},
lookup: function(){
var modelId = $("#modelId").val()
var model = new MyModel({id:modelId});
model.fetch({
success: function(){
window.location.hash = 'view/'+model.id;
},
error: function(){
$("#error").text('Cannot view model');
$("#error").slideDown();
}
});
},
});
return indexView
});
What I can't figure out is that it seems like the better option is for the index view to look up the model (so it can display an error message if the user asks for a model that doesn't exist, and also to keep the router cleaner). But the problem is that the router now has no reference to the model when the view/:id router is triggered. How is it supposed to get a hold of the model in the view() function?
I guess it could do another fetch, but that seems redundant and wrong. Or maybe there is supposed to be some global object that both the router and the view share (that the index view could put the model in), but that seems like tight coupling.
You can do something like this. You could do something similar with a collection instead of a model, but it seems like you don't want to fetch/show the whole collection?
With this type of solution (I think similar to what #mpm was suggesting), your app will handle browser refreshes, back/forward navigation properly. You basically have a MainView, which really acts more like a app controller. It handles events triggered either by the router, or by user interaction (clicking lookup or a back-to-index button on the item view).
Credit to Derick Bailey for a lot of these ideas.
In the Router. These are now only triggered if the user navigates by changing a URL or back/forward.
index: function() {
Backbone.trigger('show-lookup-view');
},
view: function(id) {
var model = new MyModel({id: id});
model.fetch({
success: function(){
Backbone.trigger('show-item-view', model);
},
error: function(){
// user could have typed in an invalid URL, do something here,
// or just make the ItemView handle an invalid model and show that view...
}
});
}
In new MainView, which you would create on app startup, not in router:
el: 'body',
initialize: function (options) {
this.router = options.router;
// listen for events, either from the router or some view.
this.listenTo(Backbone, 'show-lookup-view', this.showLookup);
this.listenTo(Backbone, 'show-item-view', this.showItem);
},
changeView: function(view) {
if(null != this.currentView) {
// remove() instead of undelegateEvents() here
this.currentView.remove();
}
this.currentView = view;
this.$el.html(view.render().el);
},
showLookup: function(){
var view = new IndexView();
this.changeView(view);
// note this does not trigger the route, only changes hash.
// this ensures your URL is right, and if it was already #index because
// this was triggered by the router, it has no effect.
this.router.navigate('index');
},
showItem: function(model){
var view = new ItemView({model: model});
this.changeView(view);
this.router.navigate('items/' + model.id);
}
Then in IndexView, you trigger the 'show-item-view' event with the already fetched model.
lookup: function(){
var modelId = $("#modelId").val()
var model = new MyModel({id:modelId});
model.fetch({
success: function(){
Backbone.trigger('show-item-view', model);
},
error: function(){
$("#error").text('Cannot view model');
$("#error").slideDown();
}
});
},
I don't think this is exactly perfect, but I hope it could point you in a good direction.

Backbone boilerplate: "this.model is undefined"

I'm a backbone newbie, so I'm sort of fumbling on getting an app set up. I'm using the backbone-boilerplate (https://github.com/tbranyen/backbone-boilerplate) and github-viewer (https://github.com/tbranyen/github-viewer) as a reference, though when running I seem to be getting a "this.model is undefined".
Here is my current router.js:
define([
// Application.
"app",
//Modules
"modules/homepage"
],
function (app, Homepage) {
"use strict";
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
initialize: function(){
var collections = {
homepage: new Homepage.Collection()
};
_.extend(this, collections);
app.useLayout("main-frame").setViews({
".homepage": new Homepage.Views.Index(collections)
}).render();
},
routes:{
"":"index"
},
index: function () {
this.reset();
this.homepage.fetch();
},
// Shortcut for building a url.
go: function() {
return this.navigate(_.toArray(arguments).join("/"), true);
},
reset: function() {
// Reset collections to initial state.
if (this.homepage.length) {
this.homepage.reset();
}
// Reset active model.
app.active = false;
}
});
return Router;
}
);
And my homepage.js module:
define([
"app"
],
function(app){
"use strict";
var Homepage = app.module();
Homepage.Model = Backbone.Model.extend({
defaults: function(){
return {
homepage: {}
};
}
});
Homepage.Collection = Backbone.Collection.extend({
model: Homepage.Model,
cache: true,
url: '/app/json/test.json',
initialize: function(models, options){
if (options) {
this.homepage = options.homepage;
}
}
});
Homepage.Views.Index = Backbone.View.extend({
template: "homepage",
el: '#mainContent',
render: function(){
var tmpl = _.template(this.template);
$(this.el).html(tmpl(this.model.toJSON()));
return this;
},
initialize: function(){
this.listenTo(this.options.homepage, {
"reset": function(){
this.render();
},
"fetch": function() {
$(this.el).html("Loading...");
}
});
}
});
return Homepage;
});
Thanks in advance for the help!
Update: After much googling (you should see how many tabs I have open), I think I made a little bit of headway, but still no luck. I updated my router to have the following:
app.useLayout("main-frame").setViews({
".homepage": new Homepage.Views.Index()
}).render();
I made a number of modifications to my homepage.js module to now look like this:
define([
"app",
["localStorage"]
],
function(app){
"use strict";
var Homepage = app.module();
Homepage.Model = Backbone.Model.extend({
defaults: function(){
return {
homepage: {}
};
}
});
Homepage.Collection = Backbone.Collection.extend({
//localStorage: new Backbone.LocalStorage("Homepage.Collection"),
refreshFromServer: function() {
return Backbone.ajaxSync('read', this).done( function(data){
console.log(data);
//save the data somehow?
});
},
model: Homepage.Model,
cache: true,
url: '/app/json/test.json',
initialize: function(options){
if (options) {
this.homepage = options.homepage;
}else{
//this.refreshFromServer();
}
}
});
Homepage.Views.Index = Backbone.View.extend({
template: "homepage",
el: '#mainContent',
initialize: function(){
var self = this;
this.collection = new Homepage.Collection();
this.collection.fetch().done( function(){
self.render();
});
},
render: function(){
var data = this.collection;
if (typeof(data) === "undefined") {
$(this.el).html("Loading...");
} else {
$(this.el).html(_.template(this.template, data.toJSON()));
}
return this;
}
});
return Homepage;
});
As you can see, I have localStorage code but commented out for now because I just want to get everything working first. The ultimate goal is to have an initial call that loads data from a JSON file, then continues afterwards using localStorage. The app will later submit data after the user does a number of interactions with my app.
I am getting the main view to load, though the homepage module isn't populating the #mainContent container in the main view.
I did all of the googling that I could but frustrated that it's just not sinking in for me. Thanks again for looking at this and any feedback is appreciated!
I think your class hierarchy is a bit wonky here. Your instance of Homepage.Collection is actually assigning a homepage property out of options, for instance. Then you pass an instance of Homepage.Collection into Homepage.Views.Index as the homepage option... It's a bit hard to follow.
That said, it seems to me your problem is simply that you aren't supply a model option when you construct your Homepage.Views.Index:
new Homepage.Views.Index(collections)
collections doesn't have a model property, and thus I don't see how this.model.toJSON() later on in the view can have a model to access. Basically, you seem to want Homepage.Views.Index to handle a collection of models, not just one. So you probably need a loop in your render function that goes over this.collection (and you should change your construction of the view to have a collection option instead of homepage option).
If I'm missing something here or I'm unclear it's because of this data model oddness I mentioned earlier. Feel free to clarify how you've got it reasoned out and we can try again :)
This example code you have is a little bit confusing to me, but I think the problem lies in the following two lines of code:
".homepage": new Homepage.Views.Index(collections)
$(this.el).html(tmpl(this.model.toJSON()));
It looks like you pass a collection to the view, but in the view you use this.model, hence the error "this.model is undefined", since it is indeed undefined.
If you aren't in any rush, may I suggest that you start over. It seems you are trying too much too quickly. I see that you have backbone, requirejs (or some other module loader), and the boilerplate, which is a lot to take in for someone new to backbone. Trust me, I know, because I am relatively new, too. Maybe start with some hello world stuff and slowly work your way up. Otherwise, hacking your way through bits of code from various projects can get confusing.

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.

backbone router and collection issue

I have a collection and I need to access a model in the collection when a route is fired:
App.Houses = Backbone.Collection.extend({
model: App.House,
url: API_URL,
})
App.houseCollection = new App.Houses()
App.houseCollection.fetch();
App.HouseDetailRouter = Backbone.Router.extend({
routes: {
'': 'main',
'details/:id': 'details',
},
initialize: function() {
},
main: function() {
App.Events.trigger('show_main_view');
},
details: function(id) {
model = App.houseCollection.get(id);
console.log(model);
App.Events.trigger('show_house', model);
},
});
The result of that console.log(model) is undefined. I think that this is the case because the collection has not finished the fetch() call?
I want to attach the model to the event that I am firing so that the views that respond to the event can utilize it. I might be taking a bad approach, I am not sure.
One of the views that responds to the event:
App.HouseDetailView = Backbone.View.extend({
el: '.house-details-area',
initialize: function() {
this.template = _.template($('#house-details-template').html());
App.Events.on('show_house', this.render, this);
App.Events.on('show_main_view', this.hide, this);
},
events: {
'click .btn-close': 'hide',
},
render: function(model) {
var html = this.template({model:model.toJSON()});
$(this.el).html(html);
$(this.el).show();
},
hide: function() {
$(this.el).hide();
App.detailsRouter.navigate('/', true);
}
});
EDIT: Somewhat hacky fix: See details()
App.HouseDetailRouter = Backbone.Router.extend({
routes: {
'': 'main',
'details/:id': 'details',
},
initialize: function() {
},
main: function() {
App.Events.trigger('show_main_view');
},
details: function(id) {
if (App.houseCollection.models.length === 0) {
// if we are browsing to website.com/#details/:id
// directly, and the collection has not finished fetch(),
// we fetch the model.
model = new App.House();
model.id = id;
model.fetch({
success: function(data) {
App.Events.trigger('show_house', data);
}
});
} else {
// if we are getting to website.com/#details after browsing
// to website.com, the collection is already populated.
model = App.houseCollection.get(id);
App.Events.trigger('show_house', model);
}
},
});
Since you are using neither the callbacks nor events to know when the collection's fetch call completes, perhaps fetching the collection is generating an error, or the model you want is not included in the server response, or you are routing to the view before fetch has completed.
As to your approach, here are some miscellaneous tips:
better to pass the model to the view in the view's constructor's options parameter. render() takes no arguments and I think it is unconventional to change that.
Always return this from render() in your views
You can move your this.template = _.template code to the object literal you pass to extend. This code only needs to be run once per app load, not for each individual view
For now the simplest thing may be to instantiate just a model and a view inside your details route function, call fetch on the specific model of interest, and use the success callback to know when to render the view.

Resources