Backbone boilerplate: "this.model is undefined" - backbone.js

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.

Related

Require.js module not seeing Backbone Router.js

In this simple Require/Backbone app
https://github.com/thisishardcoded/require-prob
Why does app.js see Router but TestView.js not?
Here is the first line of app.js
define(['router'],function (Router) {
and here is the first line of TestView.js
define(['backbone','router'],function(Backbone,Router){
Check out the repo for full details, download, run and check console log if you feel so inclined
Thanks!
Jim
More: Ok, the answer is - because of the order in which it is loaded and even if that were altered I have a circular dependency don't I? TestView needs Router, Router needs TestView.
In which case the solution might be
var r=require('router);
r.navigate or whatever
but, that seems like a shame that Router is not directly accessible everywhere and, is the above method good practice anyway?
Surely it happens because of circular dependency. To resolve it, you either pass router to view's constructor and remove router dependency from view's module, or use require('router') in your view.
1st option, router.js:
app_router.on('route:test', function () {
var testView = new TestView({router: app_router});
testView.render();
})
1st option, view.js:
define(['backbone'], function(Backbone){
var TestView=Backbone.View.extend({
el: '#test',
initialize: function() {
// get router from constructior options
this.router = this.options.router
},
render: function(){
this.$el.html('<p>Foo!</p>');
console.log("TestView.js does not find 'Router',", this.router);
}
});
return TestView;
});
2nd option, view.js:
define(['backbone','router'], function(Backbone, router){
// at this point router module is not loaded yet so router is undefined
var TestView=Backbone.View.extend({
el: '#test',
initialize: function() {
// at this point router module is loaded and you may access it with require call
this.router = require('router');
},
render: function(){
this.$el.html('<p>Foo!</p>');
console.log("TestView.js does not find 'Router',", this.router);
}
});
return TestView;
});
2nd option is also described here: http://requirejs.org/docs/api.html#circular
You should define baseUrl property in your main.js file that contains RequireJS config.
In this way all paths to modules in your application will be relative to that baseUrl.
See:
http://requirejs.org/docs/api.html#jsfiles
http://requirejs.org/docs/api.html#config-baseUrl
I downloaded and inspected your code. Following could be the issues:
require.js only works with AMDs. Since backbone no longer supports AMD. You will need to use AMD enabled version of Backbone. You can get it here
TestView is the dependency in you Router. So it loads before the Router is loaded.
You might want to improve the coding pattern. Here is my suggestion:
App.js
define([
'backbone',
'router',
], function(Backbone, MainRouter){
'use strict';
var AppView = Backbone.View.extend({
initialize: function(){
App.router = new MainRouter();
Backbone.history.start();
}
});
return AppView;
});
Router.js
define([
'backbone',
'view/TestView'
], function(Backbone, TestView){
var Main = Backbone.Router.extend({
routes: {
'test': 'test'
},
test: function(){
new TestView({
// pass model or collection to the view
// model: new TestModel // remember to require
});
}
});
return Main;
});
EDIT
Listening to events:
// in main.js
var window.Vent = {};
_.extend(window.Vent, Backbone.Events);
// now in any view you can trigger a event
$('something').on('click', function(){
window.Vent.trigger('somethinghappened', this);
// this is reference to current object
});
// now in other view you can do
window.Vent.on('somethinghappened', this.run, this);
// this in the end is the reference we passed when event was triggered
run: function(obj){
//this function will run when the event is triggered
// obj is the object who triggered the event
}
PS: why do you want to use router in view?? I have built quite a few backbone apps. Never needed to do so.
You can use available Backbone.history.navigate to achieve your goal easier, because Router.navigate is a simple wrapper for it. Consider this part of Backbone source:
navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options);
return this;
},

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.

Fetch backbone collection fail from django-tastypie

I'm using backbone-tastypie from https://github.com/PaulUithol/backbone-tastypie, and I can't fetch a collection data.
Thats my code:
var User = Backbone.Model.extend({
url: '/api/v1/user'
});
var HoraExtra = Backbone.Model.extend({
url: '/api/v1/horasextra/'
});
var HorasExtra = Backbone.Collection.extend({
url: '/api/v1/horasextra/',
model: HoraExtra
});
var Horas = new HorasExtra();
var activeUser = new User();
var HorasExtraView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
},
render: function() {
var plantilla = Handlebars.compile($("#horas_extra_template").html());
var html = plantilla(Horas);
this.$el.html(html);
console.log(JSON.stringify(Horas));
}
});
var HorasExtraWidget = new HorasExtraView({el: $('#base')});
Horas.fetch({
data: {
"usuario__id": 2,
"hor_com__month": 11
}
});
HorasExtraWidget.render();
And that's the result of console.log(JSON.stringify(Horas):
[]
Many thanks
I would suggest to you that you take tastypie, backbone and build your own simple project in 1-2 days.
Then you will get to know basic issues of coupling those 2 frameworks.
Without that knowledge it is pointless you try out other people's projects and then wonder "what isn't working".
And from my personal experience, both tastypie and javascript are pretty straightforward and are easy to couple.
And I am NOT an expert.
couple of points: try explicitly fetching models with fetch, manipulate fetched model from success callback, and watch your model url's, forward slashes in them etc.
Seems your collection is empty.
See in console if there is a GET call to server.
If not, you're not actually fetching anything from the server.
I used https://github.com/amccloud/backbone-tastypie and then your code should look like this, I guess. Not tested.
var HorasCollection = Backbone.Tastypie.Collection.extend({
url: '/api/v1/horasextra/'
});
var HorasExtraView = Backbone.View.extend({
el: $('#base'),
entries: new HorasCollection(),
render: function() {
var that = this;
this.entries.fetch({
success: function(entries){
console.log("Entries", entries.models);
// var t = _.template(template, {hello: 'world'});
// that.$el.html(t);
},
error: function(model, response){
console.log("Error", response);
}
});
}
});
If you're trying to create a table for your data, try my jquery plugin :)
https://github.com/sspross/tastybackbonepie

loading multiple views in init function breaks one or the other

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);
}

Structuring my backbone.js application

I'm in the process of creating a Backbone.js app using Require.js. Each view file corresponds to one resource (e.g. 'News'). Within each view file, I declare a backbone
view for each action ('index', 'new', etc). At the bottom of the view file I receive
the necessary info from the router and then decide which view to instantiate (based on the info passed in from the router).
This all works well, but it requires lots of code and doesn't seem to be the 'backbone.js way'. For one thing, I'm rellying on the url to manage state. For another, I'm not using _.bind which pops up in a lot of backbone.js examples. In other words, I don't think I'm doing it right, and my code base smells... Any thoughts on how to structure my app better?
router.js
define([
'jquery',
'underscore',
'backbone',
'views/news'],
function($, _, Backbone, newsView){
var AppRouter = Backbone.Router.extend({
routes:{
'news':'news',
'news/:action':'news',
'news/:action/:id':'news'
},
news: function(action, id){
newsView(this, action, id).render();
}
});
var intialize = function(){
new AppRouter;
Backbone.history.start()
};
return{
initialize: initialize;
};
}
news.js ('views/news')
define([
'jquery',
'underscore',
'backbone',
'collections/news',
'text!templates/news/index.html',
'text!templates/news/form.html'
], function($, _, Backbone, newsCollection, newsIndexTemplate, newsFormTemplate){
var indexNewsView = Backbone.View.extend({
el: $("#content"),
initialize: function(router){
...
},
render: function(){
...
}
});
var newNewsView = Backbone.View.extend({
el: $("#modal"),
render: function(){
...
}
});
...
/*
* SUB ROUTER ACTIONS
*/
var defaultAction = function(router){
return new newsIndexView(router);
}
var subRouter = {
undefined: function(router){return defaultAction(router);},
'index': function(router){ return defaultAction(router);},
'new': function(){
return new newNewsView()
},
'create': function(router){
unsavedModel = {
title : $(".modal-body form input[name=title]").val(),
body : $(".modal-body form textarea").val()
};
return new createNewsView(router, unsavedModel);
},
'edit': function(router, id){
return new editNewsView(router, id);
},
'update': function(router, id){
unsavedModel = {
title : $(".modal-body form input[name=title]").val(),
body : $(".modal-body form textarea").val()
};
return new updateNewsView(router, id, unsavedModel);
},
}
return function(router, action, id){
var re = /^(index)$|^(edit)$|^(update)$|^(new)$|^(create)$/
if(action != undefined && !re.test(action)){
router.navigate('/news',true);
}
return subRouter[action](router, id);
}
});
While I feel like it's important to emphasize that there isn't really a "Backbone.js way", it does seem like you're replicating work Backbone should be doing for you.
I agree that it makes sense to have a specialized Router for each independent section of your application. But it looks at first glance like what you're doing in your "sub-router" section is just recreating the Backbone.Router functionality. Your AppRouter doesn't need to deal with /news URLs at all; you can just initialize a NewsRouter with news-specific routes, and it will deal with news-related URLs:
var NewsRouter = Backbone.Router.extend({
routes:{
'news': 'index',
'news/create': 'create',
'news/update/:id': 'update',
'news/edit/:id': 'edit'
},
index: function() { ... },
create: function() { ... },
// etc
});
As long as this is initialized before you call Backbone.history.start(), it will capture URL requests for its routes, and you never have to deal with the AppRouter. You also don't need to deal with the ugly bit of code at the bottom of your view - that's basically just doing what the core Backbone.Router does for you.
I'm using require.js and backbone as well I think the main difference that i'd suggest is that each file should return just one view, model, router or collection.
so my main html page requires my main router. That router is a module that requires a few views based on each of it's routes, and a bootstrapped model. Each router method passes the relevant bootstrapped model piece to the relevant view.
From there it stays really clean as long as each file is just 1 backbone thing (model, collection, view, router) and requires just the elements it uses. This makes for a lot of js files (I have about 100 for my current project) but that's where require.js optimization comes into play.
I hope that helps.
Why don't you structure your routes like this:
routes:{
'news':'news',
'news/edit/:id':'editNews',
'news/new':'newNews',
...
}

Resources