How do I set up Backbone Routes - backbone.js

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.

Related

Testing backbone router with jasmine and sinon. Cannot call the pushState.

I was following this question to test the router. My router is really simple:
App.Router = Backbone.Router.extend({
routes:{
"": "index",
"help": "help"
},
help: function() {/* not really needed */ },
index: function(){
// does something
}
});
And this is an apptempted translation of what should be the test using jasmine with sinon:
it('triggers the "index" route', function() {
var router = new App.Router();
Backbone.history.start();
//Not calling navigate it's a problem
router.navigate('help', {
trigger : true, replace: true
});
var index = sinon.spy(router, 'index');
var spyHasPS = sinon.spy(function(
data, title, url) {
expect(url).toEqual('/');
router.index();
});
var spyNoPS = sinon.spy(function(loc, frag) {
expect(frag).toEqual('');
router.index();
});
if (Backbone.history._hasPushState) {
pushStateSpy = sinon.stub(window.history, 'pushState', spyHasPS );
// window.history.pushState();
} else if (Backbone.history._wantsHashChange) {
pushStateSpy = sinon.stub(Backbone.history, '_updateHash', spyNoPS);
//Backbone.history._updateHash(window.location, '');
}
router.navigate('', {
trigger : true, replace: true
});
expect(pushStateSpy.called).toBe(true);
expect(index.called).toBe(true);
});
This test works but I could achieve it because I navigated first on "help". "help" was just something I created to pass the test but the original question didn't do it and was passing. Did I do something wrong? I also run his test but the error I'm getting is:
Expected spy _updateHash to have been called. Error: Expected spy
_updateHash to have been called.
at null.<anonymous> (/src/test/js/spec/wfcRouter.spec.js:65:32) Expected spy index to have been called.
I believe the "problem" is in the navigate function. At a certain point in the navigate: function(fragment, options) we have this control:
fragment = this.getFragment(fragment || '');
if (this.fragment === fragment) return;
So...does it make sense to test the pushState when you just have one route (remember I added "help" just to make this test pass so I don't need it)? If it does make sense, how can I achieve this test?
It seems like what you are testing is Backbone code, but there's no need for you to test that: presumably the Backbone code has been tested plenty by Jeremy Ashkenas (and if you look at the Backbone project on GitHub you will see that he does in fact have a comprehensive test suite). So, rather than re-testing code you didn't write that's already been tested, what you really should be testing is the code you wrote.
If you agree with that principle, then you can simplify your test a great deal, down to just:
it('triggers the "index" route', function() {
var router = new App.Router();
router.index();
expect(thingThatShouldHaveHappenedInIndexRouteDidHappen).toBe(true);
});

How to prevent backbone router to call event on page load

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.

Backbone pushstate history not working

I am using backbone.js routes and i am struggling to make history to work. Here is the code i have:
$(function() {
var AppRouter = Backbone.Router.extend({
routes: {
"/": "initHome",
"home": "initHome",
"projects": "initProjects",
"project/:id" : "initProject"
}
});
// Instantiate the router
var app_router = new AppRouter;
app_router.on('route:initProject', function (id) {
// Note the variable in the route definition being passed in here
getContent("project",id);
});
app_router.on('route:initProjects', function () {
getContent("projects");
});
app_router.on('route:initHome', function () {
getContent("home");
});
// SINGLE PAGE MAGIC
$(document).on("click",".links",function(e) {
var href = $(this).attr("href");
var url = lang + "/" + href;
page = $(this).attr("data-id");
var param = $(this).attr("data-param");
if (typeof(param) == 'undefined') { param = ""; }
if(activepage != href && !main.hasClass("loadingPage")){
loader.show();
firstInit = false;
activepage = href;
res = app_router.navigate(url, true);
getContent(page,param);
}
return false;
});
Backbone.history.start({pushState: true, root: "/karlin/"});
});
Push state is working fine on click, but it wont call getContent() function when i try back/next buttons in the browser. I am an newbie to backbone, so any advice will be helpful.
Change this: res = app_router.navigate(url, true);
To this: app_router.navigate(url, {trigger: true});
I can't see any reason to create a variable "res".
IMHO you've got a convoluted implementation of Backbone. I'd suggest moving your routes to the constructor like so:
var AppRouter = Backbone.Router.extend({
routes: {
"/": "initHome",
"home": "initHome",
"projects": "initProjects",
"project/:id" : "initProject"
},
initProject: function (id) {
// Note the variable in the route definition being passed in here
getContent("project", id);
},
initProjects: function () {
getContent("projects");
},
initHome: function () {
getContent("home");
}
});
// Instantiate the router
var app_router = new AppRouter;
Also, if you set up your routes properly like in the Backbone docs,
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
you can pass parameters to the routes with traditional links. You can also move your if activePage statement to the router as a helper function for changing pages.
Router.navigate is for rare instances.
I suggest, reading the Backbone docs over and over. I learn something new every time. There's a lot there and Backbone is doing things efficiently already. No need to reinvent the wheel.
Hope this helps!
I second Andrew's answer: your use of routing is a bit odd.
If you're interested in learning more about why, as Andrew says, "Router.navigate is for rare instances", read pages 32-46 here: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf
It's part of the sample for my book on Backbone.Marionette.js, but routing concepts remain the same. In particular, you'll learn why the default trigger value is false, and why designing your app routing with that in mind will make your apps better.

Tracking Site Activity (Google Analytics) using Backbone

I am looking the best way to track the Site Activity in Google Analytics for a web app made with Backbone and Requires.
Looking At Google's Page, I found this drop-in plugin - Backbone.Analytics.
My questions are:
1) using Backbone.Analytics, should I change backbone.analytics.js in order to add _gaq.push(['_setAccount', 'UA-XXXXX-X']);?
2) Are there other possible solutions/plugins?
I prefer "do it yourself" style :) It's really simple:
var Router = Backbone.Router.extend({
initialize: function()
{
//track every route change as a page view in google analytics
this.bind('route', this.trackPageview);
},
trackPageview: function ()
{
var url = Backbone.history.getFragment();
//prepend slash
if (!/^\//.test(url) && url != "")
{
url = "/" + url;
}
_gaq.push(['_trackPageview', url]);
}
}
And you add google analytics script to your page as usual.
You shouldn't have to change anything. Just add your Google Analytics code snippet, like normal, and include Backbone.Analytics as you would any other Javascript library.
Just figured i'd share how i'm doing it. This might not work for larger apps but I like manually telling GA when to track page views or other events. I tried binding to "all" or "route" but couldn't quite get it to record all the actions that I need automajically.
App.Router = BB.Router.extend({
//...
track: function(){
var url = Backbone.history.getFragment();
// Add a slash if neccesary
if (!/^\//.test(url)) url = '/' + url;
// Record page view
ga('send', {
'hitType': 'pageview',
'page': url
});
}
});
So i just call App.Router.Main.track(); after I navigate or do anything i want to track.
Do note that I use the new Analytics.js tracking snippet which is currently in public beta but has an API so intuitive that it eliminates the need for a plugin to abstract any complexity what so ever. For example: I keep track of how many people scroll to end of an infinite scroll view like this:
onEnd: function(){
ga('send', 'event', 'scrollEvents', 'Scrolled to end');
}
Good luck.
I wrote a small post on this, hope it helps someone:
http://sizeableidea.com/adding-google-analytics-to-your-backbone-js-app/
var appRouter = Backbone.Router.extend({
initialize: function() {
this.bind('route', this.pageView);
},
routes: {
'dashboard': 'dashboardPageHandler'
},
dashboardPageHandler: function() {
// add your page-level logic here...
},
pageView : function(){
var url = Backbone.history.getFragment();
if (!/^\//.test(url) && url != ""){
url = "/" + url;
}
if(! _.isUndefined(_gaq)){
_gaq.push(['_trackPageview', url]);
}
}
});
var router = new appRouter();
Backbone.history.start();
Regarding other possible solutions/plugins, I've used https://github.com/aterris/backbone.analytics in a few projects and it works quite well as well. It also has options for a few more things like event tracking which can be handy at some point in your analytics integration.
If you use the new universal analytics.js, you can do that like this:
var Router = Backbone.Router.extend({
routes: {
"*path": "page",
},
initialize: function(){
// Track every route and call trackPage
this.bind('route', this.trackPage);
},
trackPage: function(){
var url = Backbone.history.getFragment();
// Add a slash if neccesary
if (!/^\//.test(url)) url = '/' + url;
// Analytics.js code to track pageview
ga('send', {
'hitType': 'pageview',
'page': url
});
},
// If you have a method that render pages in your application and
// call navigate to change the url, you can call trackPage after
// this.navigate()
pageview: function(path){
this.navigate(path);
pageView = new PageView;
pageView.render();
// It's better call trackPage after render because by default
// analytics.js passes the meta tag title to Google Analytics
this.trackPage();
}
}
All answers seem to be almost good, but out-of-date (Sept. 2015). Following this Google devs guide: https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications
Here's my version of the solution (I've added the suggested call to ga('set'...) ):
MyRouter = Backbone.Router.extend
...
initialize: () ->
# Track every route and call trackPage
#bind 'route', #trackPage
trackPage: () ->
url = Backbone.history.getFragment()
# Add a slash if neccesary
if not /^\//.test(url) then url = '/' + url
# Analytics.js code to track pageview
global.ga('set', {page: url})
global.ga('send', 'pageview')
...
Just posting an update to this question as it seems to be one I get a lot from backbone.js developers I know or work with who seem to fall at the last hurdle.
The Javascript:
App.trackPage = function() {
var url;
if (typeof ga !== "undefined" && ga !== null) {
url = Backbone.history.getFragment();
return ga('send', 'pageview', '/' + url);
}
};
Backbone.history.on("route", function() {
return App.trackPage();
});
The Tracking Snippet:
<head>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||
function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();
a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;
a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script',
'//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXXXXX-X', 'auto');
</script>
</head>
The Tracking Snippet should be available on any page you wish to track activity. This could be your index.html where all your content is injected, but some sites may have multiple static pages or a mix. You can include the ga('send') function if you wish, but it will only fire on a page load.
I wrote a blog post that goes in to more detail, explaining rather than showing, the full process which you can find here: http://v9solutions.co.uk/tech/2016/02/15/how-to-add-google-analytics-to-backbone.js.html

Multiple routers with backbone.js

Can I use multiple routers in backbone.js, that don't interfere with each other route-wise, without any issues? Or is there something that I should be concerned about?
Code sample:
myapp.routers.main = Backbone.Router.extend({
routes : { "": "index" },
index : function() { console.log("routed by main router");}
});
myapp.routers.another = Backbone.Router.extend({
routes : { "notmain": "index" },
index : function() { console.log("routed by another router");}
});
mainrouter = new vaikava.routers.main;
notmainrouter = new vaikava.routers.another;
Backbone.history.start();
Yes, it works just fine; the only time you'd have a problem is if they have conflicting routes. There is a workaround that makes it work that way as well, but it's a bit of a hack.
As long as you avoid having multiple routers trying to handle the same route you should be fine.

Resources