Structuring my backbone.js application - backbone.js

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',
...
}

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

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.

Accessing collection in multiple views - Backbone + RequireJS

I am working on my first RequireJS/Backbone app and I've hit a wall. There's a lot of code smell here, and I know I'm just missing on the pattern.
I have a route that shows all promotions, and one that shows a specific promotion (by Id):
showPromotions: function () {
var promotionsView = new PromotionsView();
},
editPromotion: function (promotionId) {
vent.trigger('promotion:show', promotionId);
}
In my promotions view initializer, I new up my PromotionsCollection & fetch. I also subscribe to the reset event on the collection. This calls addAll which ultimately builds a ul of all Promotions & appends it to a container div in the DOM.
define([
'jquery',
'underscore',
'backbone',
'app/vent',
'models/promotion/PromotionModel',
'views/promotions/Promotion',
'collections/promotions/PromotionsCollection',
'text!templates/promotions/promotionsListTemplate.html',
'views/promotions/Edit'
], function ($, _, Backbone, vent, PromotionModel, PromotionView, PromotionsCollection, promotionsListTemplate, PromotionEditView) {
var Promotions = Backbone.View.extend({
//el: ".main",
tagName: 'ul',
initialize: function () {
this.collection = new PromotionsCollection();
this.collection.on('reset', this.addAll, this);
this.collection.fetch();
},
render: function () {
$("#page").html(promotionsListTemplate);
return this;
},
addAll: function () {
//$("#page").html(promotionsListTemplate);
this.$el.empty().append('<li class="hide hero-unit NoCampaignsFound"><p>No campaigns found</p></li>');
this.collection.each(this.addOne, this);
this.render();
$("div.promotionsList").append(this.$el);
},
addOne: function (promotion) {
var promotionView = new PromotionView({ model: promotion });
this.$el.append(promotionView.render().el);
}
});
return Promotions;
});
Each promotion in the list has an edit button with a href of #promotion/edit/{id}. If I navigate first to the list page, and click edit, it works just fine. However, I cannot navigate straight to the edit page. I understand this is because I'm populating my collection in the initialize method on my View. I could have a "if collection.length == 0, fetch" type of call, but I prefer a design that doesn't have to perform this kind of check. My questions:
How do I make sure my collection is populated regardless of which route I took?
I'm calling render inside of my addAll method to pull in my template. I could certainly move that code in to addAll, but overall this code smells too. Should I have a "parent view" that's responsible for rendering the template itself, and instantiates my list/edit views as needed?
Thanks!
Here's one take. Just remember that there is more than one way to do this. In fact, this may not be the best one, but I do this myself, so maybe someone else can help us both!
First off though, you have a lot of imports in this js file. It's much easier to manage over time as you add/remove things if you import them like this:
define(function( require ){
// requirejs - too many includes to pass in the array
var $ = require('jquery'),
_ = require('underscore'),
Backbone = require('backbone'),
Ns = require('namespace'),
Auth = require('views/auth/Auth'),
SideNav = require('views/sidenav/SideNav'),
CustomerModel = require('models/customer/customer');
// blah blah blah...});
That's just a style suggestion though, your call. As for the collection business, something like this:
Forms.CustomerEdit = Backbone.View.extend({
template: _.template( CustomerEditTemplate ),
initialize: function( config ){
var view = this;
view.model.on('change',view.render,view);
},
deferredRender: function ( ) {
var view = this;
// needsRefresh decides if this model needs to be fetched.
// implement on the model itself when you extend from the backbone
// base model.
if ( view.model.needsRefresh() ) {
view.model.fetch();
} else {
view.render();
}
},
render:function () {
var view = this;
view.$el.html( view.template({rows:view.model.toJSON()}) );
return this;
}
});
CustomerEdit = Backbone.View.extend({
tagName: "div",
attributes: {"id":"customerEdit",
"data-role":"page"},
template: _.template( CustomerEditTemplate, {} ),
initialize: function( config ){
var view = this;
// config._id is passed in from the router, as you have done, aka promotionId
view._id = config._id;
// build basic dom structure
view.$el.append( view.template );
view._id = config._id;
// Customer.Foo.Bar would be an initialized collection that this view has
// access to. In this case, it might be a global or even a "private"
// object that is available in a closure
view.model = ( Customer.Foo.Bar ) ? Customer.Foo.Bar.get(view._id) : new CustomerModel({_id:view._id});
view.subViews = {sidenav:new Views.SideNav({parent:view}),
auth:new Views.Auth(),
editCustomer: new Forms.CustomerEdit({parent:view,
el:view.$('#editCustomer'),
model:view.model})
};
},
render:function () {
var view = this;
// render stuff as usual
view.$('div[data-role="sidetray"]').html( view.subViews.sidenav.render().el );
view.$('#security').html( view.subViews.auth.render().el );
// magic here. this subview will return quickly or fetch and return later
// either way, since you passed it an 'el' during init, it will update the dom
// independent of this (parent) view render call.
view.subViews.editCustomer.deferredRender();
return this;
}
Again, this is just one way and might be terribly wrong, but it's how I do it and it seems to work great. I usually put a "loading" message in the dom where the subview eventually renders with replacement html.

require.js / js file loads before DOM loaded

I am new in MVC framework like backbone.js. I am trying to make an application using backbone.js, require.js and underscore.js. I also added jQuery UI. There is a js file named widget.js which actually adjusts widgets like fixing position, firing some events when window re-sizes, firing ui events like button(), tab() etc.
Now the problem is when I open a page using a url directly (like http:://localhost/web/#/login), everything works well. But when I open that page from another page using navigation menu, there occurs a mass. The page looks ugly. I think this is because when I load the page using a url than widget.js loads after DOM is loaded but when I open the page using navigation, then widget.js loads before DOM is loaded. Wasted 3 days to find a solution .But no luck. Here is the code..
define([
'jQuery',
'Underscore',
'Backbone',
'text!templates/loginForm.html',
'text!templates/home.html',
'libs/custom/widget',
], function($, _, Backbone, loginForm, homeTemplate, widget){
var loginView = Backbone.View.extend({
el: $("#page"),
status: 'Failed',
serverMsg:'Mail doesn\'t match',
events:{
"click .login":"login"
},
isLogedIn:function(){
if(this.status=='Failed' || this.status==''){
console.log("error");
}else{
console.log("succss");
}
},
login:function(e){
e.preventDefault();
var info = $(".loginForm").serialize();
var url = 'http://rest.homeshop.dev/login/';
var This = this;
$.ajax({
url:url,
type:"POST",
data:info,
success:function(data){
data = $.parseJSON(data);
This.serverMsg = data.msg;
This.status = data.status;s
This.isLogedIn();
}
});
return false;
},
initialize: function(){
},
render: function(){
var data = {};
var compiledTemplate = _.template( loginForm, data);
this.el.find('.carrier').html(compiledTemplate);
}
});
return new loginView;
});
Also I did not added jquery Ui library here as I have added that library in application.js .Here is the codes for application.js
define([
'jQuery',
'Underscore',
'Backbone',
'router',
'libs/custom/jqueryUi-min',
'libs/custom/widget',
'libs/custom/layoutSwitcher'
], function($, _, Backbone, Router){
var initialize = function(){
Router.initialize();
}
return {
initialize: initialize
};
});
Can any one help me ?
I think what you want is the domReady! plugin. http://requirejs.org/docs/api.html#pageload
define(['jQuery', 'Backbone', 'Router', 'libs/custom/widget', 'domReady!']
, function($, Backbone, Router, widget, dom) {
//execute your code knowing that the DOM has loaded.
}
)
Hope this helps!

When is the right time to fetch a collection in Backbone js?

So I'm working on a backbone app, and trying to modularize things as much as I can using require.js. This has been my guide.
I'm having trouble getting my view to always fetch my collection. If I access my app from the base url (myapp.com/), and go to the route of my view, the collection is fetched. If I do not go to the view, and instead access it from myapp.com/#/campaigns, then the collection is not fetched.
Here is some relevant code.
router.js
define([
'jQuery',
'Underscore',
'Backbone',
'views/home/main',
'views/campaigns/list'
], function($, _, Backbone, mainHomeView, campaignListView ){
var AppRouter = Backbone.Router.extend({
routes: {
// Define some URL routes
'campaigns': 'showCampaigns',
// Default
'*actions': 'defaultAction'
},
showCampaigns: function(){
campaignListView.render();
},
defaultAction: function(actions){
// We have no matching route, lets display the home page
//mainHomeView.render();
}
});
var initialize = function(){
var app_router = new AppRouter;
Backbone.history.start();
};
return {
initialize: initialize
};
});
collections/campaigns.js
define([
'jQuery',
'Underscore',
'Backbone',
'models/campaigns'
], function($, _, Backbone, campaignsModel){
var campaignsCollection = Backbone.Collection.extend({
model: campaignsModel,
url: '/campaigns',
initialize: function(){
}
});
return new campaignsCollection;
});
views/campaigns/list.js
define([
'jQuery',
'Underscore',
'Backbone',
'collections/campaigns'
], function($, _, Backbone, campaignsCollection){
var campaignListView = Backbone.View.extend({
el:$('#container'),
initialize:function(){
this.collection = campaignsCollection;
this.collection.fetch();
},
render: function(){
var data = {
campaigns: this.collection,
_: _
};
$('#container').html('Campaigns length: '+data.campaigns.models.length);
}
});
return new campaignListView;
});
Any ideas on what I'm doing wrong? I believe it has something to do with calling this.collection.fetch() in the initalize function of the view. If that is the issue, where should I put fetch()?
The problem in your code is that your campaignListView fetch the collection when it is initialized and it is initialized only once. Your render method which is actually called from the router doesn't call your initialize method of campaignListView, when you change theurl your second time.
You have two options here :
1. return the class not the instance of your campaignListView and initialize it in the router :
// in your router code
showCampaigns: function(){
var campaignListViewInstance = new campaignListView(); // don't forget the brackets after your initialize function
campaignListViewInstance.render();
},
// in your campaignListView code :
return campaignListView; // without "new"
This will initialize the code everytime the router is hit.
2. place your fetch in the render method
// in your campaignListView code :
initialize:function(){
this.collection = campaignsCollection;
},
render: function(){
this.collection.fetch({success: function(collection) {
var data = {
campaigns: collection,
_: _
};
$('#container').html('Campaigns length: '+data.campaigns.models.length);
}); // this will be fetched everytime you render and also it has success callback
}
Also be aware that you should replace all your instance creating functions with brackets at the end
return new campaignListView; --> return new campaignListView();
return new campaignsCollection; --> return new campaignsCollection();
Another problem you will face is the async work of fetch. You should use success or event driven rendering of your collection, because fetch works in the background and you will have no data when you immediately call render after calling fetch.
+1 for your AMD approach.
I should really update the tutorial but it's better to not return instantiated modules. Maybe try checking out http://backboneboilerplate.com

Resources