Setting URL in Backbone.Router for QUnit testing - backbone.js

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.

Related

Which one should I use? Backbone.js Router.navigate and window.location.hash

I began learning Backbonejs recently, by reading a book. and I feel a little bit confuse about this issue.Here is a Router:
define(['views/index', 'views/login'], function(indexView, loginView) {
var SelinkRouter = Backbone.Router.extend({
currentView: null,
routes: {
'home': 'home',
'login': 'login'
},
changeView: function(view) {
if(null != this.currentView)
this.currentView.undelegateEvents();
this.currentView = view;
this.currentView.render();
},
home: function() {
this.changeView(indexView);
},
login: function() {
this.changeView(loginView);
}
});
return new SelinkRouter();
});
and this is the boot method of a application:
define(['router'], function(router) {
var initialize = function() {
// Require home page from server
$.ajax({
url: '/home', // page url
type: 'GET', // method is get
dataType: 'json', // use json format
success: function() { // success handler
runApplicaton(true);
},
error: function() { // error handler
runApplicaton(false);
}
});
};
var runApplicaton = function(authenticated) {
// Authenticated user move to home page
if(authenticated) window.location.hash='home';
//router.navigate('home', true); -> not work
// Unauthed user move to login page
else window.location.hash='login';
//router.navigate('login', true); -> not work
// Start history
Backbone.history.start();
}
return {
initialize: initialize
};
});
My question is about the runApplication part. The example of the book that I read passed router into module just like this, but it used window.location.hash = "XXX", and the router wasn't touched at all.
I thought the "navigate" method would make browser move to the page I specified, but nothing happened. Why?
And for the best practice sake, what is the best way to achieve movement between pages(or views)?
thanks for any ideas.
You could also use the static method to avoid router dependency (while using requirejs for instance).
Backbone.history.navigate(fragment, options)
This way, you just need :
// Start history
Backbone.history.start();
// Authenticated user move to home page
if(authenticated)
Backbone.history.navigate('home', true);
// Unauthed user move to login page
else
Backbone.history.navigate('login', true);
According to the documentation, if you also want to call the function belonging to a specific route you need to pass the option trigger: true:
Whenever you reach a point in your application that you'd like to save
as a URL, call navigate in order to update the URL. If you wish to
also call the route function, set the trigger option to true. To
update the URL without creating an entry in the browser's history, set
the replace option to true.
your code should look like:
if(authenticated)
router.navigate('home', {trigger: true});
Once your router is created, you also have to call
Backbone.history.start();
Backbone.history.start([options])
When all of your Routers have
been created, and all of the routes are set up properly, call
Backbone.history.start() to begin monitoring hashchange events, and
dispatching routes.
Finally the runApplication logic will be something similar to this:
var runApplicaton = function(authenticated) {
var router = new SelinkRouter();
// Start history
Backbone.history.start();
// Authenticated user move to home page
if(authenticated)
router.navigate('home', true);
// Unauthed user move to login page
else
router.navigate('login', 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.

still the navigate triggers and upate my url, the method is not calling

In my backbone app, i use the requirejs to load the js files. as well i need different views, there is no.of links are there in my drop down menu. according to the drop down menu i a adding the #url example:
http://localhost:85/bino/html/interface-I.html#projectName/project11
the navigate method works fine and updating the url, also whenever i copy and paste this url to any other browser / refresh with current hash state my router methods works fine.
But click on link in the drop down menu not working, the method not calling... what would be the reason and how can i fix this..?
my code: main js file (part of code)
var extender = _.extend({},backBone.Events);
var params ={
boardHolder :$('.boardHolder'),
column :3,
space :30,
extender :extender
};
var listApp = new routerer(params);
backBone.history.start();
extender.bind("list:selected",function(post){
listApp.navigate(post.category+'/'+post.filter);
});
my router code :
define(["backBone","singleton","listCollection","listView","listViews"],function(Backbone,singleton,listCollection,listView,listViews){
singleton.router = Backbone.Router.extend({
routes:{
"" :"appView",
"post" :"postView",
"projectName/:id" :"projectNameView",
"assignedTo/:id" :"assignedToView",
"sortBy/:id" :"sortByView"
},
initialize:function(params){
this.params = params;
this.collection = new listCollection;
console.log('i am called');
},
hashView:function(){
console.log('from hash view');
},
appView:function(){
var that = this;
// var defaultApp = new listCollection();
this.collection.fetch({
success:function(data){
new listViews({model:data,params:that.params})
}
})
},
projectNameView:function(thisView){ // not calling not sync
console.log('called',thisView); // on click not works
},
assignedToView:function(thisView){ // not calling not sync
console.log(thisView); // on click not works
},
sortByView:function(thisView){ // not calling not sync
console.log(thisView); // on click not works
}
});
return singleton.router;
})
thanks in advance.
navigate only updates the url, you also have to call the route function by setting the trigger option to true. If you'd like to update the URL without creating an entry in the browser's history, also set the replace option to true.
listApp.navigate(post.category+'/'+post.filter);
would become
listApp.navigate(post.category+'/'+post.filter, {trigger: true});

How can I create custom GET urls with params using Backbone?

I've noticed that some web sites offer Ajax-ian search that refreshes the URL and displays the GET params used, for example:
someapp.com/search/Tokyo?price_min=80&price_max=300
As a result of an Ajax GET request.
I want to know how can I accomplish this by using Backbone.js, I understand that by using backbone's push state this may be possible, am I right?
How could I define a route like that (let's say the same case, scoped to /search) for a Place model for example?
Where would I do this? in a Router or in a Model?
I appreciate all the answers regarding this topic. And I apologize in advance for not providing any code, I usually do, but this exercise will be a proof of concept I'd like to make, and I hope backbone is the right tool for the job.
Thank you!
This is a working example of the solution - jsfiddle.net/avrelian/dGr8Y/, except that jsFiddle does not allow Backbone.history.navigate method to function properly.
Suppose, we have a button
<input class="fetch-button" type="button" value="Fetch" />​
and a handler
$('.fetch-button').click(function() {
Backbone.history.navigate('posts/?author=martin', true);
});
This is our collection of posts
var Posts = Backbone.Collection.extend({
url: 'api/posts'
});
This is our Router with a custom parameter extractor
var Router = Backbone.Router.extend({
routes: {
'posts/?:filters': 'filterPosts'
},
filterPosts: function(filters){
posts.fetch({data: $.param(filters)});
},
_extractParameters: function(route, fragment) {
var result = route.exec(fragment).slice(1);
result.unshift(deparam(result[result.length-1]));
return result.slice(0,-1);
}
});
It is simplified $.deparam analog. You could use your own instead.
var deparam = function(paramString){
var result = {};
if( ! paramString){
return result;
}
$.each(paramString.split('&'), function(index, value){
if(value){
var param = value.split('=');
result[param[0]] = param[1];
}
});
return result;
};
And finally, instantiation of posts collection and router object
var posts = new Posts;
var router = new Router;
Backbone.history.start();
When a user clicks on the button address bar changes to myapp.com/s/#posts?author=martin. Please, note the sign #. We use a hash query string. Of course, you can use push state, but it is not widespread yet.

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

Resources