got in trap with Backbone Router. Imagine, i have 2 Backbone Routers:
1) RootRouter - has only one route and the only responsibility - load subRouters with RequireJS and instance it.
var RootRouter = Backbone.Router.extend({
routes: {
'*all': 'invokeSubModule'
},
invokeSubModule: function(route, args) {
require(['SubRouter'], function(subRouter) {
new subRouter()
})
}
});
2) SubRouter - standard BB router with routes hash and handlers.
var SubRouter = Backbone.Router.extend({
routes: {
'some/bar': 'doBar',
'some/foo': 'doFoo'
},
doBar: function() { ... },
doFoo: function() { ... }
});
I start the application from some/bar URL.
On start RootRouter instancing and Backbone.History starts.
As expected RootRouter - match any URL and fire invokeSubModule - async load and SubRouter instancing works as expected, but the problem is associated with some/bar SubRouter handler does not firing as page URL has not changed from last route.
Looking for solution i've found answers only for the case u load sub routers before history start, but it useless in my case.
So after some digging i've found solution - extend Backbone.Route and override route method to make possible to invoke handler if the Backbone.getHash() is equal to the route method operate with.
Backbone.Router.extend({
route: function(route, name, callback) {
...
if (!callback) callback = this[name];
/* run handler immediately if route we add is the current URL fragment */
if(routeRegexp.test(Backbone.history.getHash()) ) {
this.execute(callback, this._extractParameters(routeRegexp, routeStr));
}
Backbone.history.route(route, function(fragment) {
....
});
return this;
}
})
So i'm confused that this just a hack and may cause possible bugs in future.
So looking for best practice how to resolve such issue and critic of my solution.
Also expect as possible answer how to manage routers lazy loading without RootRouter, as in this case first route will not be fired.
i was able to replicate the behavior you need without hacking the internals of backbone routing, but i have to do some stuff in the initialization.
first i will create the main router and start backbone history in with silent option = true
var mainRouter = new RootRouter();
Backbone.history.start({silent: true});
this will start backbone history, but without routing the current url.
then i got the current fragment and saved it for later use, then navigated to the base url, then back to the original fragment
var fragment = Backbone.history.fragment;
mainRouter.navigate('reset',true);
mainRouter.navigate(fragment, true);
the bad side of this approach is that you need to do 2 routing on start up
UPDATE:
below the full sample
<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title> sample </title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script>
<script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script>
<style type="text/css">
</style>
<script>
var RootRouter = Backbone.Router.extend({
routes: {
'*all': 'invokeSubModule',
'reset': 'invokeSubModule',
},
invokeSubModule: function(route, args) {
new SubRouter();
},
navigate: function () {
Backbone.Router.prototype.navigate.apply(this, arguments);
},
execute: function(callback, args) {
console.log('execute root');
Backbone.Router.prototype.execute.apply(this, arguments);
console.log ('current fragment ' + Backbone.history.fragment);
}
});
var SubRouter = Backbone.Router.extend({
routes: {
'some/bar': 'doBar',
'some/foo': 'doFoo'
},
navigate: function () {
Backbone.Router.prototype.navigate.apply(this, arguments);
},
execute: function(callback, args) {
console.log('execute sub');
Backbone.Router.prototype.execute.apply(this, arguments);
console.log ('current fragment ' + Backbone.history.fragment);
},
doBar: function() {
$('#content').html('').append('<p>BAR</p>');
},
doFoo: function() {
$('#content').html('').append('<p>FOO</p>');
}
});
$(document).ready(function(){
var mainRouter = new RootRouter();
Backbone.history.start({silent: true});
var fragment = Backbone.history.fragment;
mainRouter.navigate('#',true);
mainRouter.navigate(fragment, true);
$('a').click(function(){
mainRouter.navigate($(this).attr('href'));
});
});
</script>
</head>
<body>
<a id='home' href="#">home</a></br>
<a id='foo' href="#/some/foo">foo</a></br>
<a id='bar' href="#/some/bar">bar</a></br>
<div id='content'>HOME</div>
</body></html>
Related
I have a problem where a page has two components but only one of them is fully rendered.
The problem seem to be related to $http. I have a angular project where I need to construct a page based on RESTful API. The pages are such that I can expect multiple requests for the same data. At the moment, the set of requests are not behaving correctly.
For the sake of the argument (and also because it is a use case), the following page makes the same request twice.
game.html:
<html ng-app="prvdApp">
<head>
<meta charset="utf-8">
<base href="/">
<title>Providence</title>
<script src="/js/angular-1.6.2.js"></script>
<script src="/data-access/data-access.service.js"></script>
<script src="/score-info/score-info.component.js"></script>
<script src="/js/game.js"></script>
</head>
<body>
<div ng-controller="gameController">
<score-info game-id="8000"></score-info>
<score-info game-id="8000"></score-info>
</div>
</body>
game.js:
angular.module('prvdApp', [
'scoreInfo',
'drivesInfo' ]);
angular.
module('prvdApp').
controller('gameController', function() {
});
score-info.component.js:
angular.module('scoreInfo', [
'dataAccess'
]);
angular.
module('scoreInfo').
component('scoreInfo', {
templateUrl : '/score-info/score-info.template.html',
controller : function ScoreInfoController(dataAccess) {
self = this;
self.$onInit = function() {
dataAccess.game(self.gameId).then(function(game) {
self.game = game;
});
}
},
bindings : {
gameId : '<'
}
});
score-info.template.html:
<div>
Data available: {{ $ctrl.game != undefined }}
</div>
data-access.component.js:
angular.module('dataAccess', []);
angular.
module('dataAccess').
service('dataAccess',
function DataAccessService($http, $q) {
self = this;
self.game = function(game_id) {
var url = '/api/game/' + game_id;
return $http.get(url, { cache: true}).then(function(response) {
return response.data;
});
}
});
The behaviour is as follows:
The page renders with the content:
Data available: false
Data available: false
After some hundreds of milliseconds the $http -request finishes, the page is updated to the following state where only the latter component is updated.
Data available: false
Data available: true
It should be noted that the behaviour is the same even if the two components are of different types with different controllers, etc.
I have main app with subapps:
main_app
|-mainRouter.js
|-sub_app
|-subAppRouter.js
subAppRouter.js extends mainRouter.js. subAppRouter.js has handler for route (e.g. /app1/item/). I have no access to subAppRouter.
Here is what I need:
In mainRouter I want to create routing that will handle all URL's from all apps.
It should handle route , make some check and in one case it should continue firing handler from subAppRouter for that url, else it should make redirect (e.g. /app2/somepage).
Could someone helps me with finding the best solution how to do it?
In other words: how to realize interceptor pattern via router in backbone?
Thanks
i will rephrase your question in points
1- you have a main router for common routes
2- you have a specialized router for some app routes
3- you need your main router to choose weather to handle the route of just forward it to sub router
to achieve this i suggest the following
1- create the main router , extending Backbone.Router
var mainRouter = Backbone.Router.extend({
routes:{
'index':'loadIndex',
//other common app routes......
},
loadIndex: function(){
//code to load index
}
});
2- then define the sub router for app,extending the main router, but notice how routes is defined
var subAppRouter = mainRouter.extend({
initialize: function(){
// here we will extend the base routes to not lose default routing, and add app special routing
_.extend(this.routes, {
'subApp/index': 'subAppIndex'
});
},
subAppIndex: function(){
// code to load sub app index
},
});
then you can use the sub router which will contains the base routing also
Here is a good article about subrouting. This works perfect for me.
Include subroute js lib in your project:
<script type="text/javascript" src="backbone.subroute.min.js"></script>
HTML body example:
App1
App2
<div class="app">
</div>
JS Code example:
var MyApp = {};
MyApp.App1 = {
Router: Backbone.SubRoute.extend({
routes: {
"": "init",
"sub1": "sub1"
},
init: function () {
console.log("app1");
$(".app").html($("<h1 />", {text: "App1"}));
$(".app").append($("<a/>", {href: "#app1/sub1", text: "sub1"}));
},
sub1: function () {
console.log("sub1");
$(".app").append($("<h2 />", {text: "sub1"}));
}
})
};
MyApp.Router = Backbone.Router.extend({
initialize: function () {
if(!MyApp.Routers){
MyApp.Routers = {};
}
},
routes: {
"app1/*subroute": "invokeApp1Router",
"app2": "app2"
},
invokeApp1Router: function (subroute) {
if(!MyApp.Routers.App1){
MyApp.Routers.App1 = new MyApp.App1.Router("app1/");
}
},
app2: function () {
console.log("app2");
$(".app").html($("<h1 />", {text: "App2"}));
}
});
$(document).ready(function () {
new MyApp.Router();
Backbone.history.start();
})
In my backbone function, i am navigating a id to routers, but the function not calling... as well i have given the different sample navigate urls to my links, those are not calling the functions..
mycode :
(function($){
var myRouter = Backbone.Router.extend({
routes:{
"":"defaultRoute", //onload it works
'#/name/:id':"nameData",
// i am calling this function on default Router
'#/project/:id':"projectData"
},
defaultRoute:function(){
console.log('default')
startRoute.navigate("#/name/3"); // i am redirecting
},
nameData:function(id){
console.log(id); // id not consoling not called this func.
},
projectData:function(project){
console.log(project);
}
});
var startRoute = new myRouter();
Backbone.history.start();
})(jQuery);
my url : http://localhost:85/router/web/#/name/3
my html :
<ul class="name">
<li>name1</li>
<li>name2</li>
<li>name3</li>
<li>name4</li>
<li>name5</li>
</ul>
any one find me the wrong this what i do here.. please
All my functions are correct, by mistrake i added the hash on the routes prams.
update function here:
(function($){
var myRouter = Backbone.Router.extend({
routes:{
"":"defaultRoute",
'name/:id':"nameData",
'product/:id':"projectData"
},
defaultRoute:function(){
console.log('i am default');
},
nameData:function(e,id){
console.log(id);
},
projectData:function(project){
console.log(project);
}
});
var startRoute = new myRouter();
Backbone.history.start();
})(jQuery);
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.
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