BackboneJS - Subviews navigation - backbone.js

I have a pretty simple mainmenu with 4 anchors and the relevant views to them. Anyhow, on one of those views I want to add a little submenu with 3 tabs, which after clicking them they show a different view. I figured out how to do it with pushState:false but what I want is a clean URL. Right now, my URL would look like http://localhost/myproject/#secondpage/subview1 or http://localhost/myproject/#secondpage/subview2 etc etc. So does anyone know how I do achieve http://localhost/secondpage no matter which subview/tab is triggered?
Im using RequireJS and HandlebarsJS (for HTML-templating)
So right now my code (snippets) look like this:
Router.js
routes: {
'': 'index',
'firstpage' : 'firstpage',
'secondpage' : 'secondpage',
'secondpage/sub1' : 'sub1',
'secondpage/sub2' : 'sub2',
'secondpage/sub3' : 'sub3',
'thirdpage' : 'thirdpage'
},
Backbone.history.start({
pushState: false
});
My HTML with the anchors:
<ul>
<li>
<a class="sub1" href="#secondpage/sub1">Bands</a>
</li>
<li>
<a class="sub2" href="#secondpage/sub2">Koncert</a>
</li>
<li>
<a class="sub3" href="#secondpage/sub3">Locations</a>
</li>
</ul>
and my View looks like
define(['backbone','handlebars', 'text!templates/SubMenu.html'],
function(Backbone,Handlebars, Template) {
'use strict';
var SubMenuView = Backbone.View.extend({
template: Handlebars.compile(Template),
initialize: function () {
_.bindAll(this);
},
render: function() {
$(this.el).html(this.template());
return this;
}
});
return SubMenuView;
}
);
Another thing is: should I move the actions to the View by setting events? I kind of tried that but it didnt work since the Views are defined in the router...
What I tried is to set pushState:true, then I removed the secondpage/sub1 thingies in my router, then in my View I wrote:
events: {
'click a.sub1': 'sub1',
},
sub1: function(event) {
event.preventDefault();
var sub1Router = new Backbone.Router();
var route = '/secondpage/';
sub1Router.navigate(route, {trigger: true});
},
but that didnt work, that gave me URL not found so...
Any help is welcome! Thanks in advance...
[UPDATE]
OK, so by request, here is my (new) router:
var Router = Backbone.Router.extend({
routes: {
'': 'index',
'firstpage' : 'firstpage',
'secondpage' : 'secondpage',
'thirdpage' : 'thirdpage'
},
initialize: function () {
var self = this;
//Views
this.mainMenuView = new MainMenuView({el:'#mainMenu'}).render();
this.subMenuView = new SubMenuView();
Backbone.history.start({
pushState: true
});
},
index: function () {
var self = this;
},
firstpage: function() {
this.firstpageView = new FirstpageView({el:'#topContent'}).render();
},
secondpage: function() {
this.secondpageView = new SecondpageView({el:'#topContent'}).render();
this.subMenuView = new SubMenuView({el:'#subMenu'}).render();
},
thirdpage: function() {
var thirdpageView = new ThirdpageView({ el:'#topContent', collection:this.categoryCollection}).render();
},
sub1: function() {
this.sub1View = new Sub1View({el:'#subContent_2'}).render();
},
sub2: function() {
this.sub2View = new Sub2View({el:'#subContent_2'}).render();
},
sub3: function() {
this.sub3View = new Sub3View({el:'#subContent_2'}).render();
}
});
return Router;
}
And my (new) View looks like:
var SubMenuView = Backbone.View.extend({
template: Handlebars.compile(Template),
events: {
'click .sub1': 'sub1',
'click .sub2': 'sub2',
'click .sub3': 'sub3',
},
sub1: function(event) {
var sub1Router = new Backbone.Router();
var route = '/secondpage';
sub1Router.navigate(route, {trigger: true});
},
sub2: function(event) {
event.preventDefault();
var sub2Router = new Backbone.Router();
var route = '/secondpage';
sub2Router.navigate(route, {trigger: true});
},
sub3: function(event) {
event.preventDefault();
var sub3Router = new Backbone.Router();
var route = '/secondpage';
sub3Router.navigate(route, {trigger: true});
},
initialize: function () {
_.bindAll(this);
},
render: function() {
$(this.el).html(this.template());
return this;
}
});
return SubMenuView;
And my (new) HTML template:
<ul>
<li>
<a class="sub1" href="/secondpage/">Sub1</a>
</li>
<li>
<a class="sub2" href="/secondpage/">Sub2</a>
</li>
<li>
<a class="sub3" href="/secondpage/">Sub3</a>
</li>
</ul>
Hope this can contribute to more input/suggestions... This is really driving me nuts which make me consider using .show() and .hide() jquery method even if i dont really want...

What you're describing is how backbone routing works, either you use '/secondpage/sub1' and use server routes, or use '#secondpage/sub1' and hit backbone routing. Either way the address bar is going to update with your URL.
One alternative option is to use a events inside the view, handling a click event and updating the view's template accordingly.
However, if you're intent on using routes then maybe have a look at clickify.js. I haven't used it myself yet, although I have it bookmarked for potential future use... sounds like it might do what you want.

try to use this in your router :
Backbone.history.navigate('secondpage');
after all the work is done (the models are fetched the views are rendered)

Related

Backbone views events firing repeatedly in routes

I have a backbonejs application that contains a router file and some views , and also i'm using requirejs to add views to routes and add templates to views. here is my codes :
routes.js
var AppRouter = Backbone.Router.extend({
routes: {
"": "getLogin",
"login": "getLogin",
"register": "getRegister",
"forget-password": "getForgetPassword"
},
getLogin: function() {
require(['views/auth/loginView'], function(view) {
view = new this.LoginView();
});
},
getRegister: function() {
require(['views/auth/registerView'], function() {
view = new this.RegisterView();
});
},
getForgetPassword: function() {
require(['views/auth/forgetPasswordView'], function() {
view = new this.ForgetPasswordView();
});
},
});
var route = new AppRouter();
Backbone.history.start();
loginView.js
var LoginView = Backbone.View.extend({
el: '#wrapper',
initialize: function() {
NProgress.start();
this.render();
},
render: function() {
require(['text!partials/auth/login.html'], function(t) {
var json = { title: 'title', formName: 'frmLogin' };
var template = _.template(t);
$('#wrapper').html(template(json));
});
NProgress.done();
},
events: {
"click #btnLogin": "login"
},
login: function(e) {
e.preventDefault();
alert('some message');
}
});
also registerView.js and forgetPasswordView.js are similar to loginView.js.
now! when i change routes multiple times and hit #btnLogn it fires alert('some message'); function multiple times...!
Have you tried un-delegating the events in the view, on route change?
You could override the route method (annotated source) in your AppRouter and run it before each route is rendered.
route: function(route, name, callback) {
view.undelegateEvents();
return Backbone.Router.prototype.route.apply(this, arguments);
}
Note: Just an idea, not tested with your code

Understanding the el in backbone

I don't quite understand how the el works in backbone.
I was under the assumption that el defaulted to body when it wasn't specified. I created a fiddle to illustrate my misunderstanding.
When I specify the el everything works fine. Unspecified returns nothing though.
http://jsfiddle.net/9R9zU/70/
HTML:
<div class="foo">
<p>Foo</p>
</div>
<div class="bar">
</div>
<script id="indexTemplate" type="text/template">
Bar?
</script>
JS:
app = {};
app.Router = Backbone.Router.extend({
routes: {
"" : "index"
},
index: function() {
if (!this.indexView) {
this.indexView = new app.IndexView();
this.indexView.render();
} else {
this.indexView.refresh();
}
}
});
app.IndexView = Backbone.View.extend({
// el: $('.bar'),
template : _.template( $('#indexTemplate').html() ),
render: function() {
this.$el.html(this.template());
return this;
},
refresh: function() {
console.log('we\'ve already been here hombre.')
}
});
var router = new app.Router();
Backbone.history.start();
If you do not specify element in the Backbone view, it will create an html node in memory, render the view into it and bind all event handlers based on that node. Then you will need to manually append it to the dom like this:
$('body').append(this.indexView.render().el);

Backbone.js model.save() fire a "too much recursion" error in underscore

I've got a problem trying to use backbone on saving my Model from a form. Here I want my my view to actually be an editing form:
(function() {
'use strict';
var YachtEditor = {};
window.YachtEditor = YachtEditor;
var template = function(name) {
return Mustache.compile($('#' + name + 'Template').html());
};
YachtEditor.Tank = Backbone.Model.extend({
defaults : {
dCapacity : "",
sType : ""
}
});
YachtEditor.Tanks = Backbone.Collection.extend({
// url: "/rest/tanks",
localStorage: new Store("tanks"),
model : YachtEditor.Tank
});
YachtEditor.TankView = Backbone.View.extend({
template: template("tank"),
events: {
'click .save' : 'save',
'click .remove' : 'remove'
},
initialize: function() {
console.log("initialize tank View :");
console.log(this.model.get("id"));
},
render: function() {
this.$el.html(this.template(this));
return this;
},
save: function() {
console.log('change');
var self = this;
var values = {
sType: self.$("#sType").val(),
dCapacity: self.$("#dCapacity").val()
};
console.log("dCapacity : " + values.dCapacity);
console.log("sType : " + values.sType);
this.model.save(values);
},
remove: function() {
this.model.destroy();
},
dCapacity : function() {
return this.model.get("dCapacity");
},
sType : function() {
return this.model.get("sType");
}
});
YachtEditor.TanksView = Backbone.View.extend({
el: $("div.tankZone"),
template: template("tanks"),
events: {
"click .add" : "addTank",
"click .clear" : "clear"
},
initialize: function() {
this.tanks = new YachtEditor.Tanks();
// this.tanks.on('all', this.render, this);
this.tanks.fetch();
this.render();
},
render: function() {
this.$el.html(this.template(this));
this.tanks.each(this.renderTank, this);
return this;
},
renderTank: function(tank) {
var view = new YachtEditor.TankView({model: tank});
$(".tanks").append(view.render().el);
return this;
},
addTank: function() {
this.tanks.create({});
this.render();
},
clear: function() {
this.tanks.each(function(tank) {
tank.destroy();
});
this.render();
}
});
...
})();
Here is the mustache template i use for each tank
<script id="tankTemplate" type="text/x-mustache-template">
<div class="tankView">
<h1>Tank</h1>
<select id="sType" value="{{ sType }}">
#for(option <- Tank.Type.values().toList) {
<option>#option.toString</option>
}
</select>
<input id="dCapacity" type="text" value="{{ dCapacity }}">
<button class="destroy">x</button>
</div>
</script>
My problem here is that this.model.save() triggers a 'too much recursion' in underscore. js. (chrome is displaying an error also.
Here is the call stack on error:
_.extend
_.clone
_.extend.toJSON
_.extend.save
_.extend.update
Backbone.sync
_.extend.sync
_.extend.save
YachtEditor.TankView.Backbone.View.extend.save
st.event.dispatch
y.handle
I suspect the save to recall the blur event but i cannot find a way to explicit it... Maybe I'm not using backbone as i should?
My problem, aside of some pointed out by Yurui Ray Zhang (thank you), was that I was using a backbone-localstorage.js from an example I found here : git://github.com/ngauthier/intro-to-backbone-js.git
The "too much recursion error" stopped to appear as soon a I replaced it with a storage I found here : https://github.com/jeromegn/Backbone.localStorage
a few things. you defined your tank model as
app.Tank = ...
but in your collection you are referencing it as:
model : YachtEditor.Tank
and in your view, you are trying to assign elements before they are rendered on the page:
this.input = {}
this.input.sType = this.$("#sType");
this.input.dCapacity = this.$("#dCapacity");
I'm not sure how your view is rendered to the page, some people, like me, like to use render() to render the template directly to the page:
render: function() {
this.$el.html(this.template(this));
//done, you should be able to see the form on the page now.
},
some others, will use something else to insert the el, eg:
//in another view
tankView.render().$el.appendTo('body');
but either way, if you want to cache your elements, you need to do it after they are rendered to the page, not in initialize.
//this method is only called after render() is called!
cacheElements: function() {
this.input = {}
this.input.sType = this.$("#sType");
this.input.dCapacity = this.$("#dCapacity");
}
I'd suggest, first, try to fix this things, and then, try to add some console log or debuggers in your readForm method to see if the values are grabbed correctly:
readForm: function() {
var input = this.input;
console.log(input.sType.val());
console.log(input.dCapacity.val());
this.model.save({
sType: input.sType.val(),
dCapacity: input.dCapacity.val()
});
},

How to get attributes from the clicked element in backbone event?

Here is my basic backbone view for changing routes. I would like to get the href attribute of the clicked link. How to do that? Here is a code bellow:
var Menu = Backbone.View.extend({
el: '.nav',
events: {
'click a' : 'changeRoute'
},
changeRoute: function(e) {
e.preventDefault();
//var href = $(this).attr("href");
router.navigate(href, true);
}
});
I am a newbie in backbone, so please have mercy :)
you can use: var element = $(e.currentTarget);
then any attributes can be called like this: element.attr('id')
so in your code above:
changeRoute: function(e) {
e.preventDefault();
var href = $(e.currentTarget).attr("href");
router.navigate(href, true);
}

Backbone view not rendering correctly on subsequent calls to route handler

I'm having a problem with rendering a backbone.js view successfully from a route handler (browser application).
My javascript module is currently setup like this:
$(function () { // DOM ready
myModule.Init();
});
var myModule = (function () {
// Models
var DonorCorpModel = Backbone.Model.extend({ });
// Collections
var DonorCorpsCollection = Backbone.Collection.extend({ model : DonorCorpModel });
// Views
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = Handlebars.compile($('#pre-sort-actions-template').html())
this.collection.bind('reset', this.render);
},
render : function () {
$(this.el).html(this.template({}));
this.collection.each(function (donorCorp) {
var donorCorpBinView = new DonorCorpBinView({
model : donorCorp,
list : this.collection
});
this.$('.donor-corp-bins').append(donorCorpBinView.render().el);
});
return this;
}
});
var DonorCorpBinView = Backbone.View.extend({
tagName : 'li',
className : 'donor-corp-bin',
initialize : function () {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.template = Handlebars.compile($('#pre-sort-actions-donor-corp-bin-view-template').html());
},
render : function () {
var content = this.template(this.model.toJSON());
$(this.el).html(content);
return this;
}
})
// Routing
var App = Backbone.Router.extend({
routes : {
'' : 'home',
'pre-sort' : 'preSort'
},
initialize : function () {
// ...
},
home : function () {
// ...
},
preSort : function () {
if (donorCorps.length < 1) {
donorCorps.url = 'http://my/api/donor-corps';
donorCorps.fetch();
}
var donorCorpsList = new DonorCorpsListView({ collection : donorCorps }).render().el;
$('#document-action-panel').empty().append(donorCorpsList);
// ...
}
});
// Private members
var app;
var donorCorps = new DonorCorpsCollection();
// Public operations
return {
Init: function () { return init(); }
};
// Private operations
function init () {
app = new App();
Backbone.history.start({ root: '/myApp/', pushState: true });
docr.navigate('/', { trigger: true, replace: true});
}
}(myModule || {}));
Everything runs just fine when I run the app...it navigates to the home view as expected. I have links setup with handlers to navigate to the different routes appropriately, and when I run app.navigate('pre-sort', { trigger: true, replace: true}) it runs just fine, and renders the expected items in the list as expected.
The problem comes when I try to go back to the home view, and then back to the pre-sort view again...the items in the list don't get displayed on the screen as I am expecting them to. I'm just getting the empty pre-sort-actions-template rendered with no child views appended to it. I've stepped through the code, and can see that the collection is populated and the items are there...but for some reason, my code isn't rendering them to the view properly, and I can't seem to figure out why or what I'm doing wrong. Any ideas?? I'm pretty new to backbone, so I'm sure this code isn't written totally right...constructive feedback is welcome. Thanks.
how is the code rendering it to the view after going back home, then to pre-sort again? could you provide some details on that? duplicate items? empty view?
Also, I like to keep an index of my sub-views when I render them so the parent view can always access them regardless of scope. I found a really nice technique for this here: Backbone.js Tips : Lessons from the trenches
A quick overview of the pattern I'm referring to is as follows:
var ParentView = Backbone.View.extend({
initialize: function () {
this._viewPointers = {};
},
render: function (item) {
this._viewPointers[item.cid] = new SubView ({
model: item
});
var template = $(this._viewPointers[item.cid].render().el);
this.$el.append(template);
}
});
var SubView = Backbone.View.extend({
initialize: function () {
this.model.on("change", this.render, this);
},
render: function () {
this.$el.html( _.template($('#templateId').html(), this.model.attributes) );
return this;
}
});
I realize this answer is rather "broad," but it will be easier to answer with more specifics if I can understand the exact issue with the rendering. Hope its of some help regardless :)

Resources