0

Is there a way to generally intercept MarionetteJS operations such as routing and model fetching?

A parallel would be $httpInterceptor in AngularJS (https://stackoverflow.com/a/11957760/41887)

The use case I'm interested in handling is user authentication, to redirect to a login page/route should there be no current user authentication. While I could handle this specifically in each controller, I'd prefer to have a single general mechanism that is globally applied.

Update:

For fetching interception I've seen one suggestion as follows:

AuthenticationModule.addInitializer ->
  # Tell jQuery to watch for any 401 or 403 errors and handle them appropriately
  $.ajaxSetup {
    statusCode: {
      401: -> window.location.replace('#login')
      403: -> window.location.replace('#denied')
    }
  }  

but it doesn't work as well if you want to validate against something in addition to a HTTP response code (i.e. a user entity/representation in memory).

Community
  • 1
  • 1
Bradley Dwyer
  • 8,102
  • 5
  • 32
  • 28
  • Actually I ended up using a solution like yours, by setting a default behaviour in jquery's ajax calls and I'm pretty happy with it. Can you make an example of what you're trying to obtain? – Ingro Jul 15 '13 at 08:54

2 Answers2

1

Marionette.AppRouter extends the standard Backbone.Router, neither of them provides an interception mechanism that allow you to prevent the execution of a route callback. In need of the same features, I came up with the following code (_ is Lodash). In the original Backbone.Router.prototype.route(), a function that will be executed on a route match is created. The following code provides a wrapper for the original Backbone.Router.prototype.route() in order to replace the registered route callback with a function that will execute a chain of middleware functions. Each of these middleware can then perform any additional processing before or after execution of the original route callback, including preventing execution of further callback and raise exception.

Backbone.RouteMiddlewares = (function () {

  var Backbone = require('backbone')
    , _ = require('lodash');

  /**
   * A  wrapper for `Backbone.Router.prototype.route()` that will use route middlewares (in addition to its
   * normal callback).
   *
   * @param {Function} fn - The wrapped function (should be compatible with `Backbone.Router.prototype.route()`).
   * @param {string|RegExp} route - A route pattern or regexp.
   * @param {string} name - The name of the route.
   * @param {Function} callback - The route callback.
   * @returns {Backbone.Router}
   */
  function RouteMiddleware(fn, route, name, callback) {
    // Prepare arguments as the original Backbone.Router.prototype.route() would.
    if (!_.isRegExp(route)) route = Backbone.Router.prototype._routeToRegExp.apply(this, [route]);
    if (_.isFunction(name)) {
      callback = name;
      name = '';
    }
    if (!callback) callback = this[name];

    // Execute the wrapper `route` method, with the callback as final middleware.
    return fn.apply(this, [route, name, function () {
      executeMiddlewares.apply(this, [route, name, Backbone.history.getFragment(),  Array.prototype.slice.apply(arguments, [0]), callback]);
    }]);

  };

  /**
   * Add a route middelware.
   *
   * @param {RouteMiddleware} fn
   */
  RouteMiddleware.use = function use(fn) {
    middlewares.push.apply(middlewares, _.filter(arguments, _.isFunction));
  };

  /**
   * @callback RouteMiddleware
   * @param {RegExp} route - The matched route regexp.
   * @param {string} name - The matched route.
   * @param {string} fragment - The matched URL fragment.
   * @param {Array} args - The route arguments (extracted from `fragment`).
   * @param {Function} next - The function to call to execute the next middleware in the chain.
   */

  /**
   * @type {RouteMiddleware[]}
   */
  var middlewares = [];

  /**
   * Execute the route middlware, ending with the provided callback.
   *
   * @param {RegExp} route - The matched route regexp.
   * @param {string} name - The matched route.
   * @param {string} fragment - The matched URL fragment.
   * @param {Array} args - The route arguments (extracted from `fragment`).
   * @param {Function} callback - The route handler to execute as final middleware.
   */
  function executeMiddlewares(route, name, fragment, args, callback) {
    var index = 0;
    var h = middlewares.concat(function (route, name, fragment, args, next) {
      callback.apply(this, args);
    });
    function next(err) {
      if (err) throw err;
      h[index++].apply(this, [route, name, fragment, args, next.bind(this)]);
    }
    next.apply(this);
  }

})();

Backbone.Router.prototype.route = _.wrap(Backbone.Router.prototype.route, Backbone.RouteMiddlewares);

Using this interceptors/middlewares mechanism, redirection of the user to the login page could be achieved with the following:

Backbone.RouteMiddlewares.use(function (route, name, fragment, args, next) {
  if (App.routeRequiresAuthentication(name) && !App.isUserAuthenticated()) {
    Backbone.history.navigate('login', {trigger: true});
  }
  else {
    next();
  }
});

Note: Being able to execute code after the route callback via middlewares is redundant with the route events on the router and on Backbone.history but it comes free with the middleware pattern.

Pierre Buyle
  • 4,883
  • 2
  • 32
  • 31
0

You can make use of Backbone.sync. This sync is the function which Backbone calls on every fetch, save operation. By default it uses jquery ajax to make calls. You can override the ajax calls inside Backbone.sync function so that you can control server interaction. Here is the Backbone.Sync documentation

anil
  • 61
  • 2