How to prevent backbone router to call event on page load - backbone.js

I am a bit confused at the moment. My routes functions are being executed on the first load, which is not good in my case, because I am rendering the content with these functions and on the first load I am getting the duplicated content... Ok, I can add the control variable to prevent rendering on first init, but I would like to do it with pure backbone...
Here is my code:
var Router = Backbone.Router.extend({
routes: {
"": "home",
"home": "home",
"about": "about",
},
home: function(){
getContent("home")
},
about: function(){
getContent("about")
},
initialize: function(){
Backbone.history = Backbone.history || new Backbone.History({silent:true});
root = "kitchenV3/"+lang;
var enablePushState = true;
var pushState = !! (enablePushState && window.history && window.history.pushState);
Backbone.history.start({
silent: true,
pushState: pushState,
root: root
});
}
});
On the other side, if i remove ,,home" and ,,about" methods and write them this way, they are not executed on the first load. But what is the actual difference between these two? Is it possible to write the code like on the first example, but to prevent execution on the first load?
router.on('route:home', function(id) {
getContent("home")
});
Thank you for all answers...

From Backbone's doc:
"If the server has already rendered the entire page, and you don't want the initial route to trigger when starting History, pass silent: true."
As for the difference between your 2 examples: when you start Backbone's history in the second case, no routes are bound, so obviously no code is executed.
Edit:
Successfully tested.
You'll have an alert. Then replace Backbone.history.start() by Backbone.history.start({silent: true}) and nothing will happen.
Furthermore, digging into Backbone.history#start:
if (!this.options.silent) return this.loadUrl();
So... I don't know, but if it doesn't work for you, I'll guess we'll need more information.
Edit 2:
I've changed what I told you in the comments, and this is the result. Once again, simply remove the silent: true to see the difference.

Related

Backbonejs view binding conceptual feedback

I ran into this article (http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) and was wondering if the idea of binding and rendering views in the router after instantiating them is best practice. I have been binding my views and rendering them in my view definition.
Currently this is how I've been setting up and calling my views:
EmployeeView:
EmployeeView = Backbone.View.extend({
el: '#content',
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
}
My Router:
employeeList: function () {
var c = new EmployeeCollection
new EmployeeView( {
collection: c
});
}
It works fine. But according to the article a better practice is to do the following:
EmployeeView = Backbone.View.extend({
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
return this;
}
Router
employeeList: function () {
var c = new EmployeeCollection
$('#content').html(new EmployeeView( {collection: c}).render().el);
},
I like the solution in the article because it decouples the views from other DOM events as the article said and allows me to focus all my tweaking and customizing in one place, the router. But because I'm passing in a collection/model and need to fetch the data in my initialize my page renders twice. My questions are:
Is this really best practice?
How do I avoid calling the render twice if I want to use the suggested method?
What if I have cases where I have some front end user interaction and then need to refresh the view collection/model? Would I have to do it in my view or could that happen in the router as well?
The view you have here, and the one in the article are totally different.
In your example, the view is bound to an element in DOM (#content),
which is not a good practice, especially for beginners and causes lots of bugs that we see here every day.
For example if you create 2 instances of your view then event will starts firing multiples times and along with that all hell will break loose.
The view in the article creates a new <div> element in memory per instance, which is a good practice.
Now, to add this in DOM, newbies often do stuff like the following inside the view's render:
$('#content').html(this.$el);
This creates a global selector inside the view and makes it aware of the outer world which is not a good practice.
The article probably (I didn't read it) address this is issue and presents and alternative of adding the view element to DOM from the router, which is a good practice in my opinion.
To avoid rendering twice in the code from article you can just do:
$('#content').html(new EmployeeView( {collection: c}).el);
el being a live reference, it'll be updated when the fetch succeeds. .render().el is another common mis-understanding spread by all the existing blogs and tutorials.
Side note: Since we are discussing best practices, omitting the semicolon and parenthesis as in var c = new EmployeeCollection is not a good practice either. Go with var c = new EmployeeCollection();
You got it almost right. You're just rendering it twice, which I don't think is the right way to go, as there is no point.
EmployeeView = Backbone.View.extend({
template:template,
initialize: function(){
console.log("Will print second");
this.collection.fetch({ reset: true });
this.collection.on('reset', this.appendEmployees, this);
},
render: function(){
//this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('Will print 3rd. render called');
return this;
}
appendEmployees: function(){
console.log("Will print 4th. Appending employees");
$(this.el).html(Mustache.to_html(this.template, {employee_list: this.collection.toJSON() });
}
})
Router
employeeList: function () {
var c = new EmployeeCollection()
var view = new EmployeeView({ collection: c });
console.log("Will print 1st");
$('#content').html(view.render().el);
}
First, when you do view.render().el it will append view's element (which will be empty by that time) to #content
Second, you're executing appendEmployees function when collection resets. By the time this will happen your element will already be placed in the DOM.
In case you need to refresh, it can be done inside the view, by calling the appendEmployees function, or even by resetting your collection. Or if you navigate to the same route via backbone, the whole process will be repeated hence your collection will be called again, and the page will render from beginning. So it comes down to your preferences on when/why you'd choose one over the other. Hope this helps.

How do I set up Backbone Routes

This is the first time I am using Backbone and I seem to be stuck on the basics, so bear with me.
I just want to use Backbone for Routing, I'm currently testing it within the News section of my site but I can't get the routes to trigger the functions I want.
Here' my code:
var NewsRouter = Backbone.Router.extend({
routes: {
"*news": "init",
"news:tmpl": "loadTemplate",
},
init: function(params) {
//$("#main").load("/news/all");
console.log('news called')
},
loadTemplate: function(tmpl) {
console.log('loadTemplate function called')
}
});
var news_router = new NewsRouter;
Backbone.history.start();
I have this route working fine:
mysite.dev/news/ - console shows "news called"
mystic.dev/news/interviews - should call loadTemplate()
What am I mssing?
You missed slash after "news" in the route for 'loadTemplate':
"news/:tmpl": "loadTemplate",
Note that in your case router is configured only for hash-based navigation (like '#news/interviews' ). You may enable URL-based navigation by specifying additional options for 'start' method:
Backbone.history.start({ pushState: true });
I've tested. This works.
var NewsRouter = Backbone.Router.extend({
routes: {
"news": "init",
"news/:tmpl": "loadTemplate",
},
init: function(params) {
//$("#main").load("/news/all");
alert('news called');
},
loadTemplate: function(tmpl) {
alert('loadTemplate function called: ' + tmpl);
}
});
var news_router = new NewsRouter;
Backbone.history.start();
Only updated below part.
routes: {
"news": "init",
"news/:tmpl": "loadTemplate",
},
Basically, you also need to remove * (asterisk) apart from missing slash as answered by #Vitaliy Fedorchenko.
Backbone code is not as complex as jQuery. It's pretty readable. So best thing is go to code and read rather than finding documentation. I don't understand regex as much, but if you see splatParam variable, I think it is treating asterisk as wild match. Anyone can please correct me if I'm wrong.

Backbone Router & Deep Linking

My single page web application consists of 4-5 views stacked vertically, when a user chooses a menu item, the page will scroll to the appropriate view. When you come into the application for the first time this is not a problem, however if you deep link to a menu item my page throws a fit because it's trying to access properties of an element that does not yet exists.
The problem I am having is understanding why the elements do not exist at the time the router is trying to scroll the page.
If you load / and then select home no problems, but if you directly hit #home via browser that when I get jQuery undefined errors.
Uncaught TypeError: Cannot read property 'top' of undefined
Inside router I am instantiating and rendering all of my views within the initialize function. The idea is the initialize will always happen before any of my routes, clearly not the case.
Again I've read a few threads that show how to have a before and after function for either all routes of individual routes but even using that approach scrollToById fails because it doesn't know what $(id) is at the time of being called.
define(function (require, exports, module) {
var Backbone = require('backbone');
return Backbone.Router.extend({
initialize: function(){
require(['ui/menu/menu','ui/home/home', 'ui/samples/samples', 'ui/resume/resume', 'ui/contact/contact'],
function(Menu, Home, Samples, Resume, Contact){
var menu = new Menu();
menu.render();
var home = new Home();
home.render();
var samples = new Samples();
samples.render();
var resume = new Resume();
resume.render();
var contact = new Contact();
contact.render();
});
},
routes: {
'' : 'init',
'home' : 'home',
'samples' : 'samples',
'resume' : 'resume',
'contact' : 'contact'
},
init: function(){
},
home: function (){
this.scrollToById($(".home-container"));
},
samples: function(){
this.scrollToById($(".samples-container"));
},
resume: function(){
this.scrollToById($(".resume-container"));
},
contact: function(){
this.scrollToById($(".contact-container"));
},
scrollToById: function(id) {
var val = $(id).offset().top - 127;
$('html, body').animate({
scrollTop: val
}, 2000);
}
});
});
Appreciate any tips or advice.
I think the routes event handlers in the router are getting initialized at the same time as the initialize function. Because of this, route events are getting triggered before the DOM elements are rendered.
I would try making a new function outside of Router that contains everything currently inside the initialize function. Then the final thing in that function can be to create an instance of the router. This will ensure that no routes events are called until your scripts and DOM are loaded.

Setting URL in Backbone.Router for QUnit testing

I have a Backbone.Router based myRouter, that calls certain set() methods based on the URL:
var MyRouter = Backbone.Router.extend({
routes: {
"doSomething/:value": "doSetterOnModel",
},
doSetterOnModel: function(value) {
// does some set(value) calls on my Model
},
});
I would like to test now if with the model gets updated (set()) correctly using a QUnit test:
test( "update Model via URL change", function() {
var value = "newValue";
var url = "http://localhost/#/doSomething/" + value;
//does not work as it moves page away from QUnit's tests.html page
window.location = url;
// does also not work because it moves away from QUnit's tests.html page
myRouter.navigate(url);
deepEqual( myModel.get(key), value, "Value on Model was not updated by URL change");
});
How can I set the URL in Qunit in order to test if my Router performs the right action for a certain URL?
When I want to test my routers I usually bypass the document navigation altogether and simply call Backbone.history.loadUrl, which is responsible for matching URL fragments to a router methods:
Backbone.history.loadUrl("#/doSomething");
The additional benefit here is that you don't need to call Backbone.history.start in your test code. Note also the use or a relative url. Specifying the http://localhost part is not necessary.
Tiny demo here.

What to be done to prevent the router URL being used directly on the address bar by user?

Have done some working samples using Backbone Router, but is there a way to protect the routes being used directly on the address bar? And also when the user press the back button on the browser, the routes doesn't get cleared and creates issues. What is the best solution for this?
I think I see what you're saying - you want to force the user to enter your site through a certain (home) page. Is that correct?
This is useful, for example, when you're building a mobile-optimized-web-app, and you always want users to enter through a splash screen. What I'll do is set a 'legitEntrance' property to my router, and check for it on every route, as so:
APP.Router = Backbone.Router.extend({
legitEntrance: false,
// Just a helper function
setLegitEntrance: function() {
this.legitEntrance = true;
},
// Send the user back to the home page
kickUser: function() {
this.navigate("home", {trigger:true});
},
routes : {
...
},
// Example router function: Home page
routeToHome: function() {
this.setLegitEntrance();
var homeView = APP.HomeView.extend({ ... });
homeView.render();
},
// Example router function: some other internal page
routeToSomeOtherInternalPage: function() {
if(!this.legitEntrance) {
this.kickUser();
return;
}
var someOtherInternalView = APP.SomeOtherInternalView.extend({
...
});
someOtherInternalView.render();
}
....
});
I'm sure this code could be cleaned up some, but you get the general idea. Hope it helps.

Resources