Good pattern for doing additive routes in Backbone - backbone.js

I have looked at subrouting ideas like those in BackboneMVC and Backbone Marionette.
I think I'm looking for something a little different.
Has anyone come up with good patterns for, not subroutes, but additive routes?
Say, for example, you have a large profile lightbox that could display on any screen. You'd like to add it to browser history and have the url be able to recreate it. So you might have these urls:
'dashboard/lightbox/profile'
'game/3/lightbox/profile'
In the first route, it should do all of the behaviors for the dashboard route, then apply behaviors for an additive lightbox/profile route. This way the lighbox opens and the dashboard is in the background.
In the second case, it should similarly do all the behaviors for the game/3 route, then open the lightbox on top of that.
Is this a pattern anyone has heard of or implemented?
I can't think of a way to do this without using splats, like this:
routes: {
'dashboard/*path': 'showDashboard',
'game/:id/*path': 'showGame'
},
showDashboard: function(path) {
//Show the dash
this._checkIfLightboxShouldOpen(path)
},
showGame: function(id, path) {
//Show the correct game based on id
this._checkIfLightboxShouldOpen(path)
},
_checkIfLightboxShouldOpen(path) {
// Parse the path string for lightbox behaviors
}
Is there a better way?

I needed this for a recent project, I plan to release this code as open source at some point, but you can do something like this:
Create a global router to handle all routing:
App.GlobalRouter = Backbone.Router.extend({
initialize: function(){
this._routes = {};
},
registerRoute: function(route, rootRoute){
var rootName;
if(rootRoute) {
route = rootRoute + '/' + route;
rootName = this.registerRoute(rootRoute);
}
if(!_.isRegExp(route))
route = this._routeToRegExp(route);
var name = this._routes[route] ? this._routes[route] : _.uniqueId('r');
this._routes[route] = name;
this.route(route, name, function(){});
if(rootName) {
this.on('route:'+name, function(){
var args = slice(arguments);
this.trigger.apply(this, ['route:' + rootName].concat(args));
}.bind(this));
}
return name;
}
});
Then create a single one:
App.globalRouter = new App.GlobalRouter();
Then create a modified router to extend all your routers from:
App.Router = Backbone.Router.extend({
constructor: function (options){
options = options || {};
if(options.root) this.root = options.root;
this.globalRouter = App.globalRouter;
Backbone.Router.apply(this, [options]);
},
route: function(route, name, callback, root){
if(!App.globalRouter) return false;
// If callback is root param
if(callback && !_.isFunction(callback)) {
root = callback;
callback = null;
}
// If no name is callback param.
if(_.isFunction(name)) {
callback = name;
name = '';
}
if(!callback)
callback = this[name];
var router = this;
var roots = root || this.root;
if(roots && !_.isArray(roots)) roots = [roots];
if(roots) {
_.each(roots, function(root){
var globalName = App.globalRouter.registerRoute(route, root);
router.listenTo(App.globalRouter, 'route:'+globalName, function(){
var args = slice(arguments);
var callbackArgs = args.slice(callback && -callback.length || 0);
callback && callback.apply(router, callbackArgs);
router.trigger.apply(router, ['route:' + name].concat(callbackArgs));
router.trigger('route', name, callbackArgs);
});
});
} else {
var globalName = App.globalRouter.registerRoute(route);
router.listenTo(App.globalRouter, 'route:'+globalName, function(){
var args = slice(arguments);
var callbackArgs = args.slice(callback && -callback.length || 0);
callback && callback.apply(router, callbackArgs);
router.trigger.apply(router, ['route:'+name].concat(callbackArgs));
router.trigger('route', name, callbackArgs);
});
}
return this;
}
});
From here you can create as many routers that are required and register them on the same route, also you can create a router that has route routes to listen on, so in your case you would have probably 2 or 3 routers, here is an example of what you could do:
var defaultRouter = App.Router.extend({
routes: {
'dashboard': 'showDashboard',
'game/:id': 'showGame'
},
showDashboard: function() {},
showGame: function(id) {},
});
var profilerRouter = App.Router.extend({
root: [
'dashboard',
'game/:id'
],
routes: {'profile', 'showProfile'},
showProfile: function(){//Show lightbox}
});
This will listen for /dashboard or /game/:id and call that funciton on defaultRouter that is listening. Then if the /profile is on the end of the url for either of the routes is will catch that and run the showProfile function on the profileRouter.
NOTE: I've quickly modified the code take from my project to change some of the name/namespace issues, so you might need to check that I haven't missed anything, but the code should be right otherwise
Updated Example:
If the user navigates to /game/:id it will call the defaultRouter > showGame with param :id.
If the user navigates to /game/:id/profile it will call the defaultRouter > showGame with param :id. It will also call profileRouter > showProfile but with no params (ie. it doesn't send the :id from the /game/:id root).

Related

override pivot view _render function in custom module odoo 11

I want to override "_render: function ()" function from pivot_renderer.js file in web but not working in custom module. Here is the code i am implementing in my custom module:-
odoo.define('MY_CUSTOM_MODULE_NAME.renderer', function (require) {
"use strict";
var PivotRenderer = require('web.PivotRenderer');
var field_utils = require('web.field_utils');
var core = require('web.core');
var _t = core._t;
PivotRenderer.include({
init: function(parent, state, params) {
this._super.apply(this, arguments);
},
_render: function () {
if (!this._hasContent()) {
// display the nocontent helper
this.replaceElement(QWeb.render('PivotView.nodata'));
return this._super.apply(this, arguments);
}
if (!this.$el.is('table')) {
// coming from the no content helper, so the root element has to be
// re-rendered before rendering and appending its content
this.renderElement();
}
var $fragment = $(document.createDocumentFragment());
var $table = $('<table>').appendTo($fragment);
var $thead = $('<thead>').appendTo($table).addClass("CLASS_NAME");
var $tbody = $('<tbody>').appendTo($table);
var nbr_measures = this.state.measures.length;
var nbrCols = (this.state.mainColWidth === 1) ?
nbr_measures :
(this.state.mainColWidth + 1) * nbr_measures;
for (var i=0; i < nbrCols + 1; i++) {
$table.prepend($('<col>'));
}
this._renderHeaders($thead, this.state.headers);
this._renderRows($tbody, this.state.rows);
// todo: make sure the next line does something
$table.find('.o_pivot_header_cell_opened,.o_pivot_header_cell_closed').tooltip();
this.$el.html($table.contents());
return this._super.apply(this, arguments);
},
});
});
In the above, i want to add a class in the header for calling my custom css "var $thead = $('').appendTo($table).addClass("CLASS_NAME");" with this syntax but it is not reflecting in my custom module. Although, for testing, I have implemented same class in default web module and it is working fine. The issue is in custom module.
So how to solve this issue? Is there any other way for calling class or i am doing it in a wrong way?
var $thead = $('').addClass("CLASS_NAME").appendTo($table);
This will work in my case. You can try it.

Lighthouse/Service Worker, how to return http 200 when offline

My application currently uses webpack,angular js, and a service worker.
Using sw-precache plugin to create my service worker.
https://www.npmjs.com/package/sw-precache-webpack-plugin
The service worker caching is going well and I can see my static resources being fetched from serviceworker.js from chrome dev tools.
Now when I run the lighthouse report I am getting the following error still :
URL responds with a 200 when offline
https://github.com/GoogleChrome/lighthouse
In Dev tools when I switch on offline, I can actually see my page load. Some errors in console for some 3rd party scripts failing. Is this the reason for not getting url response 200 because I have some console errors from 3rd party i.e. sample error :
GET https://fonts.googleapis.com/css?family=Roboto+Slab:300,400,700 net::ERR_INTERNET_DISCONNECTED
What exactly is this audit looking for, and how can I achieve it ?
Edit : I added a picture of my network tab when I turn on offline, as I said the page loads fine. I notice my sw.js get's loaded from disk cache which I don't notice on other sites so could be something there.
Also here is sw.js content
'use strict';
var precacheConfig = [["/css/app.styles.77e2a0c3e7ac001193566741984a07f0.css","77e2a0c3e7ac001193566741984a07f0"],["/css/vendor.styles.582e79ead0684a8fb648ce9e543ad810.css","582e79ead0684a8fb648ce9e543ad810"],["/favicon.ico","70ef569d9a12f6873e86ed57d575cf13"],["/fonts/MaterialIcons-Regular.eot","e79bfd88537def476913f3ed52f4f4b3"],["/fonts/MaterialIcons-Regular.svg","a1adea65594c502f9d9428f13ae210e1"],["/fonts/MaterialIcons-Regular.ttf","a37b0c01c0baf1888ca812cc0508f6e2"],["/fonts/MaterialIcons-Regular.woff","012cf6a10129e2275d79d6adac7f3b02"],["/fonts/MaterialIcons-Regular.woff2","570eb83859dc23dd0eec423a49e147fe"],["/icons/launcher-icon-2x.png","91896b953c39df7c40b4772100971220"],["/icons/launcher-icon-3x.png","0aee2add7f56559aeae9555e495c3881"],["/icons/launcher-icon-4x.png","b164109dd7640b14aaf076d55a0a637b"],["/images/aa_logo_only.png","b5b46a8c2ead9846df1f1d3035634310"],["/images/developer.png","e8df747b292fe6f5eb2403c7180c31da"],["/images/facebook.png","8ab42157d0974099a72e151c23073022"],["/images/home-bg.jpeg","0a0f7da8574b037463af2f1205801e56"],["/images/logo.png","e8712312e08ca427d79a9bf34aedd6fc"],["/images/map.png","af3443ef4ab2890cae371c7a3de437ed"],["/images/pattern.png","114d593511446b9a4c6e340f7fef5c84"],["/images/twitter.png","99da44949cd33e16d2d551d42559eaf2"],["/index.html","1e9b5c4b3abba7e13d8d28c98cfb3bb5"],["/js/app.d9ada27616bf469d794d.js","8e2fc74de7d5c122ab8f0aca7e31b075"],["/js/vendor.d9ada27616bf469d794d.js","3bbba4569b6f3b88881b0533260905fe"],["/manifest.json","4bea29155995b63a9f2855637c0fe74c"]];
var cacheName = 'sw-precache-v2-45-' + (self.registration ? self.registration.scope : '');
var ignoreUrlParametersMatching = [/^utm_/];
var addDirectoryIndex = function (originalUrl, index) {
var url = new URL(originalUrl);
if (url.pathname.slice(-1) === '/') {
url.pathname += index;
}
return url.toString();
};
var createCacheKey = function (originalUrl, paramName, paramValue,
dontCacheBustUrlsMatching) {
// Create a new URL object to avoid modifying originalUrl.
var url = new URL(originalUrl);
// If dontCacheBustUrlsMatching is not set, or if we don't have a match,
// then add in the extra cache-busting URL parameter.
if (!dontCacheBustUrlsMatching ||
!(url.toString().match(dontCacheBustUrlsMatching))) {
url.search += (url.search ? '&' : '') +
encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue);
}
return url.toString();
};
var isPathWhitelisted = function (whitelist, absoluteUrlString) {
// If the whitelist is empty, then consider all URLs to be whitelisted.
if (whitelist.length === 0) {
return true;
}
// Otherwise compare each path regex to the path of the URL passed in.
var path = (new URL(absoluteUrlString)).pathname;
return whitelist.some(function(whitelistedPathRegex) {
return path.match(whitelistedPathRegex);
});
};
var stripIgnoredUrlParameters = function (originalUrl,
ignoreUrlParametersMatching) {
var url = new URL(originalUrl);
url.search = url.search.slice(1) // Exclude initial '?'
.split('&') // Split into an array of 'key=value' strings
.map(function(kv) {
return kv.split('='); // Split each 'key=value' string into a [key, value] array
})
.filter(function(kv) {
return ignoreUrlParametersMatching.every(function(ignoredRegex) {
return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
});
})
.map(function(kv) {
return kv.join('='); // Join each [key, value] array into a 'key=value' string
})
.join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
return url.toString();
};
var hashParamName = '_sw-precache';
var urlsToCacheKeys = new Map(
precacheConfig.map(function(item) {
var relativeUrl = item[0];
var hash = item[1];
var absoluteUrl = new URL(relativeUrl, self.location);
var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false);
return [absoluteUrl.toString(), cacheKey];
})
);
function setOfCachedUrls(cache) {
return cache.keys().then(function(requests) {
return requests.map(function(request) {
return request.url;
});
}).then(function(urls) {
return new Set(urls);
});
}
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return setOfCachedUrls(cache).then(function(cachedUrls) {
return Promise.all(
Array.from(urlsToCacheKeys.values()).map(function(cacheKey) {
// If we don't have a key matching url in the cache already, add it.
if (!cachedUrls.has(cacheKey)) {
return cache.add(new Request(cacheKey, {credentials: 'same-origin'}));
}
})
);
});
}).then(function() {
// Force the SW to transition from installing -> active state
return self.skipWaiting();
})
);
});
self.addEventListener('activate', function(event) {
var setOfExpectedUrls = new Set(urlsToCacheKeys.values());
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.keys().then(function(existingRequests) {
return Promise.all(
existingRequests.map(function(existingRequest) {
if (!setOfExpectedUrls.has(existingRequest.url)) {
return cache.delete(existingRequest);
}
})
);
});
}).then(function() {
return self.clients.claim();
})
);
});
self.addEventListener('fetch', function(event) {
if (event.request.method === 'GET') {
// Should we call event.respondWith() inside this fetch event handler?
// This needs to be determined synchronously, which will give other fetch
// handlers a chance to handle the request if need be.
var shouldRespond;
// First, remove all the ignored parameter and see if we have that URL
// in our cache. If so, great! shouldRespond will be true.
var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching);
shouldRespond = urlsToCacheKeys.has(url);
// If shouldRespond is false, check again, this time with 'index.html'
// (or whatever the directoryIndex option is set to) at the end.
var directoryIndex = 'index.html';
if (!shouldRespond && directoryIndex) {
url = addDirectoryIndex(url, directoryIndex);
shouldRespond = urlsToCacheKeys.has(url);
}
// If shouldRespond is still false, check to see if this is a navigation
// request, and if so, whether the URL matches navigateFallbackWhitelist.
var navigateFallback = '';
if (!shouldRespond &&
navigateFallback &&
(event.request.mode === 'navigate') &&
isPathWhitelisted([], event.request.url)) {
url = new URL(navigateFallback, self.location).toString();
shouldRespond = urlsToCacheKeys.has(url);
}
// If shouldRespond was set to true at any point, then call
// event.respondWith(), using the appropriate cache key.
if (shouldRespond) {
event.respondWith(
caches.open(cacheName).then(function(cache) {
return cache.match(urlsToCacheKeys.get(url)).then(function(response) {
if (response) {
return response;
}
throw Error('The cached response that was expected is missing.');
});
}).catch(function(e) {
// Fall back to just fetch()ing the request if some unexpected error
// prevented the cached response from being valid.
console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
return fetch(event.request);
})
);
}
}
});
Some data like
https://fonts.googleapis.com/css?family=Roboto+Slab:300,400,700
does not support offline mode download these file manually and add them with local path again.

2 way binding issues with directives, controllers and services

This is bugging me a bit.
I have a service that handles logo panels and a function that is used to navigate between the different panels.
When getPanels is invoked it sets the currentPanel, index and length on the service when all promises have completed (see $q.all in the getPanels method):
.service('ConfiguratorLogoService', ['$q', 'UploadService', 'LogoService', 'ArrayService', 'SvgService', function ($q, uploadService, logoService, arrayService, helper) {
// Private function to build a file array
var _buildFileArray = function (panels, files) {
//--- Omitted for brevity ---//
};
// Create our service
var service = {
// Create our arrays
panels: [],
files: [],
currentPanel: null,
index: 0,
length: 0,
// Get our panels
getPanels: function (container, garmentId) {
// Create a deferred promise
var deferred = $q.defer();
// Create our arrays
var panels = []
files = [],
promises = [];
// If we have a container
if (container) {
// Get the containers children
var children = container.children()
// Loop through our panel's children
for (var i = 0; i < children.length; i++) {
// Get the current child
var child = angular.element(children[i]),
childId = child.attr('id'),
childTitle = helper.extractText(childId, ':', 1);
// Create our item
var panel = {
id: childId,
title: childTitle
};
// Try to get our item
promises.push(logoService.get(garmentId, panel.id).then(function (response) {
// If we have any data
if (response) {
// Add the file to our array
files.push(response);
}
}));
// Add our child to the array
panels.push(panel);
}
}
// After all the promises have been handled
$q.all(promises).then(function () {
// Get our files
service.files = _buildFileArray(panels, files);
service.panels = panels;
service.currentPanel = panels[0];
service.length = panels.length;
// Resolve our promise
deferred.resolve({
files: service.files,
panels: panels
});
});
// Return our promise
return deferred.promise;
},
// Get our next panel
navigateNext: function () {
// Create a deferred promise
var deferred = $q.defer();
// Get the next index or reset if we reached the end of our list
service.index = service.index === (service.length - 1) ? 0 : service.index += 1;
// Set our active panel
service.currentPanel = service.panels[service.index];
console.log(service.index);
// Resolve our promise
deferred.resolve();
// Return our promise
return deferred.promise;
},
// Get our previous panel
navigatePrevious: function () {
// Get the previous index or set to the end of our list
service.index = service.index === 0 ? service.length - 1 : service.index -= 1;
// Set our active panel
service.currentPanel = service.panels[service.index];
},
// Removes the file from azure
remove: function (index) {
//--- Omitted for brevity ---//
}
};
// Return our service
return service;
}])
which is fine, it works and the first panel is selected.
So, I have a controller, which is attached to a directive. The controller looks like this:
.controller('ConfiguratorLogosDirectiveController', ['ConfiguratorLogoService', 'RowService', function (service, rowService) {
var self = this;
// Set our current panel
self.currentPanel = service.currentPanel;
self.index = service.index;
self.length = service.length;
// Initialization
self.init = function (container, garmentId) {
// Get our panels
return service.getPanels(container, garmentId).then(function (response) {
self.panels = response.panels;
self.files = response.files;
// If we have any panels
if (self.panels.length) {
// Set our current panel
self.currentPanel = service.currentPanel;
self.index = service.index;
self.length = service.length;
}
// Return our response
return response;
})
};
// Map our service functions
self.upload = service.upload;
self.next = service.navigateNext;
self.previous = service.navigatePrevious;
self.remove = service.remove;
}])
As you can see, when I get my panels, I set the currentPanel, index and length on the controller itself which I didn't think I would have to do because when the controller is invoked, it already has a reference to the service values. I figured 2 way binding would come into play and when the service values update, the controller would update too.
Anyway, I update the values after the getPanels method completes successfully. In my directive I have this:
// Invoke on controller load
controller.init(container, scope.garmentId).then(function (response) {
// Map our properties
scope.panels = controller.panels;
scope.files = controller.files;
scope.currentPanel = controller.currentPanel;
scope.index = controller.index;
scope.length = controller.length;
});
which again works fine. In my template I can see the first panel and it looks fine.
So, then came the next step which was my navigate functions. I started with next which I have modified for testing purposes so I can output the controller.index as well as the console.log in the service navigation function.
The directive function looks like this:
scope.next = function () {
controller.next().then(function () {
console.log(controller.index);
});
};
When this method is invoked, I can see in my console that the service increases the index by 1 but the controller still shows 0 which means that 2 way binding is not working.
I am about to update my method in the controller to push the currentPanel and index to the controller, but before I do I thought I would ask here first.
So, does anyone know why my 2 way binding isn't working?
So my current workaround works, but I just don't like it.
In my directive I have done this:
scope.next = function () {
controller.next().then(function () {
console.log(controller.index);
scope.currentPanel = controller.currentPanel;
scope.index = controller.index;
scope.length = controller.length;
});
}
and in the directive controller I have done this:
self.next = function () {
// Try to navigate forward
return service.navigateNext().then(function () {
// Set our current panel
self.currentPanel = service.currentPanel;
self.index = service.index;
self.length = service.length;
console.log(self.index);
});
}
and in my service, it looks the same as before:
// Get our next panel
navigateNext: function () {
// Create a deferred promise
var deferred = $q.defer();
// Get the next index or reset if we reached the end of our list
service.index = service.index === (service.length - 1) ? 0 : service.index += 1;
// Set our active panel
service.currentPanel = service.panels[service.index];
console.log(service.index);
// Resolve our promise
deferred.resolve();
// Return our promise
return deferred.promise;
},
This works, but surely this is not the way it should work.
I have figured it out thanks to this article.
I just had to create an object in my directive and bind the values to that.
Doing that fixed the issues.

navigate route with querystring

Will Backbone.Router.navigate set test to true:
var test = false;
var Router = Backbone.Router.extend({
routes: {
'posts': 'showPosts'
},
showPosts: function () {
test = true;
}
});
router = new Router();
Backbone.history.start();
router.navigate('posts?foo=3', {trigger: true});
assert.ok(test);
Eg, will posts?foo=3 fragment will match the posts route by default, or do I have to set another route for that, for example: posts?*querystring?
Thank you
PS: I know there exist the backbone-query-parameters but I want to know just for backbone.
You need to add another route with that expecting parameter :
routes: {
'posts?foo=:foo' : 'showPosts',
'posts': 'showPosts'
},
showPosts: function (foo) {
if(typeof foo != 'undefined'){
// foo parameters was passed
}
test = true;
}
update
You could define the general route to return all the query string and then parse it in the handler :
routes: {
'posts': 'showPosts',
'posts?*queryString' : 'showPosts'
},
showPosts: function (queryString) {
var params = parseQueryString(queryString);
if(params.foo){
// foo parameters was passed
}
}
...
// and the function that parses the query string can be something like :
function parseQueryString(queryString){
var params = {};
if(queryString){
_.each(
_.map(decodeURI(queryString).split(/&/g),function(el,i){
var aux = el.split('='), o = {};
if(aux.length >= 1){
var val = undefined;
if(aux.length == 2)
val = aux[1];
o[aux[0]] = val;
}
return o;
}),
function(o){
_.extend(params,o);
}
);
}
return params;
}
update 2
Here's a live demo to see the code in action.
Just to complement the previous answers, instead of defining two routes that have the same callback, like:
routes: {
'posts': 'showPosts',
'posts?*querystring': 'showPosts'
}
You could have only one route to keep the code cleaner:
routes: {
'posts(?*querystring)': 'showPosts'
}
Backbone docs:
Routes can contain parameter parts, :param, which match a single URL component between slashes; and splat parts *splat, which can match any number of URL components.
If you still want to keep the functionality without the matching you can define two routes
routes: {
'posts': 'showPosts',
'posts?*querystring': 'showPosts'
}
showPosts: function(querystring) {
if (querystring) {
// here you can parse your querystring, for your case the querystring variable is
// 'foo=3'
}
//here you'll show posts according to the querystring (or lack thereof)
}
Here's another take, still using lodash (underscore). Removed the _.map, added a bit of verbosity to the variables, and stripped out the starting '?' if present:
function parseQueryString(queryString)
{
if (!_.isString(queryString))
return
queryString = queryString.substring( queryString.indexOf('?') + 1 )
var params = {}
var queryParts = decodeURI(queryString).split(/&/g)
_.each(queryParts, function(val)
{
var parts = val.split('=')
if (parts.length >= 1)
{
var val = undefined
if (parts.length == 2)
val = parts[1]
params[parts[0]] = val
}
})
return params
}
RFC 3986 "syntax for URIs" states that query parameters shold come before hash fragment.
In URIs a hashmark # introduces the optional fragment near the end of the URL. The generic RFC 3986 syntax for URIs also allows an optional query part introduced by a question mark ?. In URIs with a query and a fragment, the fragment follows the query.
I have this issue handling a redirect I am getting from the server i.e. "http://foo.com/main.html?error=errormessage#site". I would like to route on the query but can't see a way to write the backbone route expression to handle this url. For now I just route on the hash and check for a query by parsing location.search.

Backbone.js — Call method before/after a route is fired

I want a setup/teardown method to be called before and after a route is fired in my Backbone.js router, respectively. Has anyone created an elegant way of doing this?
_.wrap is not a solution, if You have for example 20 routes you have to wrap them all.
But you can do this with metaprogramming
class Backbone.FlexRouter extends Backbone.Router
route: (route, name, handler) ->
super route, name, ->
#trigger "route:before"
handler()
#trigger "route:after"
UPD: I believe in JS it should be something like this (but I didn't tested it)
var rp = Backbone.Router.prototype
rp.routeWithoutEvents = rp.route
rp.route = function(route, name, handler){
var that = this
this.routeWithoutEvents(route, name, function(){
that.trigger("route:before")
handler()
that.trigger("route:after")
})
}
Have you considered _.wrap?
Here is the simple one, overriding the Backbone.Router itself
(function () {
_.extend(Backbone.Router.prototype, Backbone.Events, {
route: function (route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function (fragment) {
var args = this._extractParameters(route, fragment);
if (this.before && _.isFunction(this.before)) {
this.before(fragment);
}
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
if (this.after && _.isFunction(this.after)) {
this.after(fragment);
}
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
}
});
}).call(this);
Focus on the lines
if (this.before && _.isFunction(this.before)) {
this.before(fragment);
}
AND
if (this.after && _.isFunction(this.after)) {
this.after(fragment);
}
You can modify the lines according to your needs
And here is the client code using the new Backbone.Router class
var appRouter = Backbone.Router.extend({
routes: {},
before: function(){
//your code here
return true;
}
});
Alexey's answer is almost right, but there are a few subtle things that are missing.
class ApplicationRouter extends Backbone.Router
route: (route, name, callback = null) ->
callback = #[name] if ! callback
super route, name, ->
#trigger 'route:before'
result = callback && callback.apply(#, arguments)
#trigger 'route:after'
return result
This plugin does what you want. It works with 0.5.3. I'm not certain if it works with 0.9.1 yet or not.
https://github.com/angelo0000/backbone_filters
I came across this problem earlier and I thought I would share my solution for inserting "middleware" into the Backbone routing flow. The goal was to reroute users to various flows depending on some condition, e.g., feature flags, session handling, etc..
Backbone.ProtectedRouter = Backbone.Router.extend({
/*
* Subclass of Router that monkeypatches route in order to protect certain
* routes.
*
* If you want to add a protected route, add it to the protectedRoutes
* object in this form:
* route: { method: fn, assertion: fn, args: [args..] }
*
* * method => the method to call if the assertion is true (the route should
* be protected in the given scenario)
*
* * assertion => the function that decides whether or not the route
* should be rendered
*
* * args => the arguments to be passed to method
*/
route: function(route, name, handler) {
var _this = this;
Backbone.Router.prototype.route(route, name, function(){
var boundHandler = _.bind(handler, _this),
attrs, method, args, dfd;
attrs = _.has(_this.protectedRoutes, route) ? _this.protectedRoutes[route] : null;
if ( attrs && !attrs.assertion() ) {
// In this scenario my flows all return Deferreds
// you can make this event based as well.
dfd = _this[attrs.method].apply(_this, attrs.args.concat([route]));
dfd.then(boundHandler);
} else
boundHandler.apply(_this, arguments);
});
}
});
From there you can simply extend the Backbone.ProtectedRouter with a protectedRoutes hash as so:
var router = Backbone.ProtectedRouter.extend({
protectedRoutes: {
'home': {
assertion: function() { return is_logged_in; },
method: 'renderLogin',
args: ['some_arg']
}
},
routes: {
'home': 'renderHome'
},
...
});
In this scenario, if a request is made for the home route and is_logged_in is false, the renderLogin method is invoked and passed 'some_arg'. After the flow, renderLogin would return a resolved Deferred that causes the original handler (renderHome) to be invoked.
I hope this helps. I'm very open to suggestions as well! :)
I came across this need recently (to check user is authenticated). Unfortunately Backbone doesn't give us a before/after event, so you will need to overwrite or extend the Router.route. Doesn't feel very clean since you have to copy from source code and edit there, but is the only way I found. Below Backbone default code (1.0.0) and marked my custom code:
Backbone.Router.prototype.route = function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
// here my custom code
callback = _.wrap(callback, _.bind(function(cb) {
if (name == 'login' || sessionModel.authenticated()) {
_.bind(cb, this)();
} else {
this.navigate('login', {trigger: true});
}
}, this));
// finish my custom code
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
callback && callback.apply(router, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
});
return this;
};
Notice _.wrap and _.bind so this is the one that you would expect when using the router. Otherwise I was getting a "this is undefined" error.
ethnagnawl and Alexey are both correct; _.wrap is the right solution but if you have a bunch of routes and write them in the normal backbone fashion it'll be a pain. I realized you can do this:
var Pages = {}
Pages.loginPage = function(){ ... }
Pages.mainPage = function(){ ... }
Instead of defining your route handlers directly in Router.extend, load them into an object and then do this:
_.map(Pages,function(func,name){
Pages[name] = _.wrap(func,function(funky){
// Save original arguments
var args = Array.prototype.slice.call(arguments,1);
// Do stuff before the route
funky(args);
// Do stuff after the route
});
});
This also makes it pretty easy to check for the function name if you need to treat a subset of them differently or something. Then, because it's just an object, you can do this:
var myRouter = Backbone.Router.extend({
routes: ... /* as usual */
}).extend(Pages);
And you're done.
One nice advantage of this is it involves no messing with the Backbone prototypes, so even if a version update changes something it won't bite you.
after doing lot more manipulations. i came to a solution that i have given below......
Here ur original root function ...
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
callback && callback.apply(router, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
});
return this;
}
Now look at this code & change the "route" function to your original Backbone.js...
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
// takes matched route & fragment as like 'route1'
var args = router._extractParameters(route, fragment);
// extracts arguments if exists
// here yours self invoking function or other function starts....
(function(){
// do something
if ( true ) // condition satisfies then route to the given Route
{
callback && callback.apply(router, args);
}
else{
name='route2'; // change name of route
window.location.hash = 'route2';
callback= function(){
// optional callback if u want
}
callback && callback.apply(router, args); // route to ur custome Route
}
})();
});
return this;
}
----- Thank You --------
Love 2 write Dirty Codes !
#xy....
Here is a JavaScript version that works with what I've got;
var rp = Backbone.Router.prototype;
rp.routeWithoutEvents = rp.route;
rp.route = function(route, name, callback) {
if (!callback) callback = this[name];
this.routeWithoutEvents(route, name, function() {
this.before.apply(this);
callback.apply(this,arguments);
this.after.apply(this);
});
};
It's based on Alexey Petrushin's and Jonathan Tran's solutions.
I could not find an easy way to intercept the routing event before the route handler is called.
My solution is to extend the Router component, adding a registerBeforeRouting method and editing the route method (I took it from the Backbone 1.0 and it worked, YMMV with different Backbone versions).
Before the router is created:
var rp = Backbone.Router.prototype;
rp.registerBeforeRouting = function (callback) {
this._beforeRoutingCallback = callback;
};
rp.route = function (route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
// Edit starts here
// This will trigger the callback previously set
if (typeof router._beforeRoutingCallback === 'function') {
router._beforeRoutingCallback();
}
// Edit stops here.
callback && callback.apply(router, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
});
return this;
}
Then, during router initialization:
this.registerBeforeRouting(function() {
console.log("Hello world");
});
I tried the aforementioned approaches, and they somehow just didn't work for me (probably for my lack of in depth understanding of neither backbone, nor javascript in general).
I did manage to do the trick in some other manner, if that's of any interest to anyone out there:
What I actually end up doing was simply extending the View and overriding the render function just one time.
MyApp.BindedView = Backbone.View.extend({
_realRender : null,
initialize : function(){
//validating user is logged in:
if(Backbone.history.fragment != 'login' && !myUser.authenticated())
{
console.log('not authorized, redirecting');
var self = this;
this._realRender = this.render;
this.render = function(route,name,callback){
appRouter.navigate('login');
self.render = self._realRender;
}
return;
}
this.delegateEvents();
}
});
The execute method was added to be overridden for this purpose. See this example extracted from the backbonejs homepage:
var Router = Backbone.Router.extend({
execute: function(callback, args, name) {
if (!loggedIn) {
goToLogin();
return false;
}
args.push(parseQueryString(args.pop()));
if (callback) callback.apply(this, args);
}
});

Resources