Skip to content

Instantly share code, notes, and snippets.

@pascalpp
Last active August 29, 2015 14:10
Show Gist options
  • Save pascalpp/44c873527f4b182a5209 to your computer and use it in GitHub Desktop.
Save pascalpp/44c873527f4b182a5209 to your computer and use it in GitHub Desktop.
Dynamic router idea
define(function(require) {
'use strict';
// override Backbone.History.prototype.route
// to allow for handlers to have a router associated with them
// this allows us to remove routes dynamically e.g. in router.destroy (see below)
Backbone.History.prototype.route = function(route, callback, router) {
this.handlers.unshift({router: router, route: route, callback: callback});
};
var Router = Marionette.AppRouter.extend({
initialize: function(options) {
Marionette.AppRouter.prototype.initialize.apply(this, arguments);
_.bindAll(this, 'checkState');
if (Backbone.History.started) {
// if this router was initialized after history started,
// it might have added a route that need to be triggered
// initialize is called first in the Marionette.AppRouter constructor, before appRoutes have been processed
// have to defer checkState so that rest of constructor can complete first
_.defer(this.checkState);
}
},
checkState: function() {
// make sure Backbone.history exists; not needed but just to be safe
if (! Backbone.history) Backbone.history = new Backbone.History();
// this tells Backbone.history to check the current fragment for any matching routes
// need to do this differently
Backbone.history.loadUrl();
},
// override Backbone.Router.route method
// the only difference is that we pass a reference to `this` to Backbone.history.route
// so this router's routes can be removed onDestroy
// need to audit this when upgrading Backbone `upgrade:backbone:audit`
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);
router.execute(callback, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
}, this);
return this;
},
// override Marionette.AppRouter.prototype._addAppRoute
// to prepend `scope` to all routes
_addAppRoute: function(controller, route, methodName) {
if (this.options.scope) {
route = this.options.scope + '/' + route;
}
Marionette.AppRouter.prototype._addAppRoute.apply(this, [controller, route, methodName]);
},
// override Marionette.AppRouter.prototype.navigate
// to prevent out-of-scope URL changes, and other automated niceties
navigate: function(fragment, options) {
/* jshint maxcomplexity: 11 */
options = options || {};
var current_fragment = Backbone.history.fragment,
scope = this.options.scope,
defaults = {},
re;
/* jshint maxdepth: 4 */
if (scope) {
if (fragment) {
// prepend scope to fragment
fragment = scope + '/' + fragment;
// if current URL ends in scope, this is additive, so set replace: true
// we only do this if options.replace isn't already set to false (using _.defaults below)
re = new RegExp(scope+'/?$', 'i');
if (current_fragment.match(re)) {
defaults.replace = true;
}
} else {
// fragment is empty, use scope as fragment
// this allows you to pass an empty string to reset URL to scope
fragment = scope;
}
// validate that requested URL is in scope
// use `force: true` to override this scope check
re = new RegExp(scope, 'i');
if (! current_fragment.match(re)) {
if (! options.force) {
log.error('Tab URL is out of scope.', scope, current_fragment);
return;
}
}
}
// if URL already exists in fragment, don't overwrite URL
// use `force: true` to force overwrite
re = new RegExp(fragment);
if (current_fragment.match(re)) {
// log('URL is already in place, no need to write');
if (! options.force) return;
}
// if URL is the same, but case is different, just replace the history state
// e.g. navigate to /mixedcase/collections, URL will be updated to /MixedCase/collections
// but it won't add an additional state to the history, so back button still works
re = new RegExp(fragment+'/?$', 'i');
if (current_fragment.match(re)) {
defaults.replace = true;
}
// apply our defaults to the passed options
// e.g. if replace is explicitly set to false, then we can't override it
_.defaults(options, defaults);
return Marionette.AppRouter.prototype.navigate.apply(this, [fragment, options]);
},
// custom destroy method
// removes any handlers that belong to this router
destroy: function() {
log('destroy router for scope', this.options.scope);
log('Backbone.history.handlers.length', Backbone.history.handlers.length);
Backbone.history.handlers = _.filter(Backbone.history.handlers, function(handler) {
return (handler.router !== this);
}, this);
log('Backbone.history.handlers.length', Backbone.history.handlers.length);
}
});
return Router;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment