Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New triggers implementation with redirect support #172

Merged
merged 11 commits into from
Jun 21, 2015
11 changes: 0 additions & 11 deletions client/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,6 @@ Route.prototype.getAllSubscriptions = function() {
return this._subsMap;
};


Route.prototype._processTriggersEnter = function(context) {
_.each(this._triggersEnter, function(fn) {
if (typeof fn === 'function') {
fn(context);
}
});
};

Route.prototype._processMiddlewares = function(context, after) {
var currentIndex = 0;
var self = this;
Expand All @@ -76,8 +67,6 @@ Route.prototype._processMiddlewares = function(context, after) {
Route.prototype.callAction = function(current) {
var self = this;

self._processTriggersEnter(current);

self._processMiddlewares(current.context, function() {
self._action(current.params, current.queryParams);
});
Expand Down
157 changes: 50 additions & 107 deletions client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ Router = function () {
// when doing subscriptions
this.safeToRun = false;

var self = this;
this.triggers = {
enter: self._getRegisterTriggersFn(self._triggersEnter),
exit: self._getRegisterTriggersFn(self._triggersExit)
};
this._redirectFn = _.bind(this._page.redirect, this._page);
this._initTriggersAPI();
};

Router.prototype.route = function(path, options, group) {
Expand All @@ -38,7 +35,8 @@ Router.prototype.route = function(path, options, group) {
var self = this;
var route = new Route(this, path, options, group);

route._handler = function (context, next) {
// calls when the page route being activates
route._actionHandle = function (context, next) {
var oldRoute = self._current.route;

self._current = {
Expand All @@ -53,7 +51,31 @@ Router.prototype.route = function(path, options, group) {
// to backward compatibility
self._current.params.query = self._current.queryParams;

self._invalidateTracker();
// we need to invalidate if all the triggers have been completed
// if not that means, we've been redirected to another path
// then we don't need to invalidate
var afterAllTriggersRan = function() {
self._invalidateTracker();
};

var triggers = self._triggersEnter.concat(route._triggersEnter);
Triggers.runTriggers(
triggers,
self._current,
self._redirectFn,
afterAllTriggersRan
);
};

// calls when you exit from the page js route
route._exitHandle = function(context, next) {
var triggers = self._triggersExit.concat(route._triggersExit);
Triggers.runTriggers(
triggers,
self._current,
self._redirectFn,
next
);
};

this._routes.push(route);
Expand Down Expand Up @@ -317,8 +339,6 @@ Router.prototype._buildTracker = function() {
Tracker.nonreactive(function() {
var currentContext = self._current;

self._processTriggersEnter(currentContext);

var isRouteChange = currentContext.oldRoute !== currentContext.route;
var isFirstRoute = !currentContext.oldRoute;
// first route is not a route change
Expand Down Expand Up @@ -353,99 +373,9 @@ Router.prototype._buildTracker = function() {
Router.prototype._invalidateTracker = function() {
this.safeToRun = true;
this._tracker.invalidate();
};

Router.prototype._getRegisterTriggersFn = function(triggers) {
var fn = function(triggerFns, options) {
options = options || {};

if (options.only && options.except) {
var message = "triggers does not support 'only' and 'except' at the same time.";
throw new Error(message);
}

_.each(triggerFns, function(fn) {
if (typeof fn !== 'function') {
return;
}

if (options.only) {
fn._only = {};
_.each(options.only, function(name) {
fn._only[name] = 1;
});
}

if (options.except) {
fn._except = {};
_.each(options.except, function(name) {
fn._except[name] = 1;
});
}

triggers.push(fn);
});
};

return fn;
};

Router.prototype._shouldCallTrigger = function(current, fn) {
var name = current.route.name;
var shouldCall;

if (typeof fn !== 'function') {
return false;
}

if (fn._only) {
shouldCall = !!fn._only[name];
} else if (fn._except) {
shouldCall = !fn._except[name];
} else {
shouldCall = true;
}

return shouldCall;
};

Router.prototype._processTriggersEnter = function(current) {
var self = this;

_.each(this._triggersEnter, function(fn) {
if (self._shouldCallTrigger(current, fn)) {
fn(current);
}
});
};

Router.prototype._processTriggersExit = function(ctx, next) {
var self = this;

_.each(self._triggersExit, function(fn) {
if (self._shouldCallTrigger(self._current, fn)) {
fn(self._current);
}
});

next();
};

Router.prototype._registerRouteTriggersExit = function(route) {
var self = this;

if (route._triggersExit.length > 0) {
// add route's exit triggers
self._page.exit(route.path, function(ctx, next) {
_.each(route._triggersExit, function(fn) {
if (typeof fn === 'function') {
fn(self._current);
}
});

next();
});
}
// we need to trigger the above invalidations immediately
// otherwise, we need to face some issues with route context swapping
Tracker.flush();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check how flush behave if a user change the route from a template helper.

That's weird. But possible.

};

Router.prototype._updateCallbacks = function () {
Expand All @@ -460,16 +390,29 @@ Router.prototype._updateCallbacks = function () {
});

_.each(self._routes, function(route) {
self._page(route.path, route._handler);
self._registerRouteTriggersExit(route);
self._page(route.path, route._actionHandle);
self._page.exit(route.path, route._exitHandle);
});

self._page.exit("*", self._processTriggersExit.bind(self));

self._page("*", function(context) {
self._notfoundRoute(context);
});
};

Router.prototype._initTriggersAPI = function() {
var self = this;
this.triggers = {
enter: function(triggers, filter) {
triggers = Triggers.applyFilters(triggers, filter);
_.extend(self._triggersEnter, triggers);
},

exit: function(triggers, filter) {
triggers = Triggers.applyFilters(triggers, filter);
_.extend(self._triggersExit, triggers);
}
};
};

Router.prototype._page = page;
Router.prototype._qs = qs;
Router.prototype._qs = qs;
108 changes: 108 additions & 0 deletions client/triggers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// a set of utility functions for triggers

Triggers = {};

// Apply filters for a set of triggers
// @triggers - a set of triggers
// @filter - filter with array fileds with `only` and `except`
// support only either `only` or `except`, but not both
Triggers.applyFilters = function(triggers, filter) {
if(!(triggers instanceof Array)) {
triggers = [triggers];
}

if(!filter) {
return triggers;
}

if(filter.only && filter.except) {
throw new Error("Triggers don't support only and except filters at once");
}

if(filter.only && !(filter.only instanceof Array)) {
throw new Error("only filters needs to be an array");
}

if(filter.except && !(filter.except instanceof Array)) {
throw new Error("except filters needs to be an array");
}

if(filter.only) {
return Triggers.createRouteBoundTriggers(triggers, filter.only);
}

if(filter.except) {
return Triggers.createRouteBoundTriggers(triggers, filter.except, true);
}

throw new Error("Provided a filter but not supported");
};

// create triggers by bounding them to a set of route names
// @triggers - a set of triggers
// @names - list of route names to be bound (trigger runs only for these names)
// @negate - negate the result (triggers won't run for above names)
Triggers.createRouteBoundTriggers = function(triggers, names, negate) {
var namesMap = {};
_.each(names, function(name) {
namesMap[name] = true;
});

var filteredTriggers = _.map(triggers, function(originalTrigger) {
var modifiedTrigger = function(context, next) {
var routeName = context.route.name;
var matched = (namesMap[routeName])? 1: -1;
matched = (negate)? matched * -1 : matched;

if(matched === 1) {
originalTrigger(context, next);
}
};
return modifiedTrigger;
});

return filteredTriggers;
};

// run triggers and abort if redirected
// @triggers - a set of triggers
// @context - context we need to pass (it must have the route)
// @redirectFn - function which used to redirect
// @after - called after if only all the triggers runs
Triggers.runTriggers = function(triggers, context, redirectFn, after) {
var abort = false;
var inCurrentLoop = true;
var alreadyRedirected = false;

for(var lc=0; lc<triggers.length; lc++) {
var trigger = triggers[lc];
trigger(context, doRedirect);

if(abort) {
return;
}
}

// mark that, we've exceeds the currentEventloop for
// this set of triggers.
inCurrentLoop = false;
after();

function doRedirect(url) {
if(alreadyRedirected) {
throw new Error("already redirected");
}

if(!inCurrentLoop) {
throw new Error("redirect needs to be done in sync");
}

if(!url) {
throw new Error("trigger redirect requires an URL");
}

abort = true;
alreadyRedirected = true;
redirectFn(url);
}
};
Loading