summaryrefslogtreecommitdiffstats
path: root/packages/kadira-flow-router/client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/kadira-flow-router/client')
-rw-r--r--packages/kadira-flow-router/client/_init.js11
-rw-r--r--packages/kadira-flow-router/client/group.js57
-rw-r--r--packages/kadira-flow-router/client/modules.js2
-rw-r--r--packages/kadira-flow-router/client/route.js125
-rw-r--r--packages/kadira-flow-router/client/router.js587
-rw-r--r--packages/kadira-flow-router/client/triggers.js112
6 files changed, 894 insertions, 0 deletions
diff --git a/packages/kadira-flow-router/client/_init.js b/packages/kadira-flow-router/client/_init.js
new file mode 100644
index 00000000..a18fdc89
--- /dev/null
+++ b/packages/kadira-flow-router/client/_init.js
@@ -0,0 +1,11 @@
+// Export Router Instance
+FlowRouter = new Router();
+FlowRouter.Router = Router;
+FlowRouter.Route = Route;
+
+// Initialize FlowRouter
+Meteor.startup(function () {
+ if(!FlowRouter._askedToWait) {
+ FlowRouter.initialize();
+ }
+});
diff --git a/packages/kadira-flow-router/client/group.js b/packages/kadira-flow-router/client/group.js
new file mode 100644
index 00000000..b93296bc
--- /dev/null
+++ b/packages/kadira-flow-router/client/group.js
@@ -0,0 +1,57 @@
+Group = function(router, options, parent) {
+ options = options || {};
+
+ if (options.prefix && !/^\/.*/.test(options.prefix)) {
+ var message = "group's prefix must start with '/'";
+ throw new Error(message);
+ }
+
+ this._router = router;
+ this.prefix = options.prefix || '';
+ this.name = options.name;
+ this.options = options;
+
+ this._triggersEnter = options.triggersEnter || [];
+ this._triggersExit = options.triggersExit || [];
+ this._subscriptions = options.subscriptions || Function.prototype;
+
+ this.parent = parent;
+ if (this.parent) {
+ this.prefix = parent.prefix + this.prefix;
+
+ this._triggersEnter = parent._triggersEnter.concat(this._triggersEnter);
+ this._triggersExit = this._triggersExit.concat(parent._triggersExit);
+ }
+};
+
+Group.prototype.route = function(pathDef, options, group) {
+ options = options || {};
+
+ if (!/^\/.*/.test(pathDef)) {
+ var message = "route's path must start with '/'";
+ throw new Error(message);
+ }
+
+ group = group || this;
+ pathDef = this.prefix + pathDef;
+
+ var triggersEnter = options.triggersEnter || [];
+ options.triggersEnter = this._triggersEnter.concat(triggersEnter);
+
+ var triggersExit = options.triggersExit || [];
+ options.triggersExit = triggersExit.concat(this._triggersExit);
+
+ return this._router.route(pathDef, options, group);
+};
+
+Group.prototype.group = function(options) {
+ return new Group(this._router, options, this);
+};
+
+Group.prototype.callSubscriptions = function(current) {
+ if (this.parent) {
+ this.parent.callSubscriptions(current);
+ }
+
+ this._subscriptions.call(current.route, current.params, current.queryParams);
+};
diff --git a/packages/kadira-flow-router/client/modules.js b/packages/kadira-flow-router/client/modules.js
new file mode 100644
index 00000000..7b734f44
--- /dev/null
+++ b/packages/kadira-flow-router/client/modules.js
@@ -0,0 +1,2 @@
+page = require('page');
+qs = require('qs');
diff --git a/packages/kadira-flow-router/client/route.js b/packages/kadira-flow-router/client/route.js
new file mode 100644
index 00000000..b82e9721
--- /dev/null
+++ b/packages/kadira-flow-router/client/route.js
@@ -0,0 +1,125 @@
+Route = function(router, pathDef, options, group) {
+ options = options || {};
+
+ this.options = options;
+ this.pathDef = pathDef
+
+ // Route.path is deprecated and will be removed in 3.0
+ this.path = pathDef;
+
+ if (options.name) {
+ this.name = options.name;
+ }
+
+ this._action = options.action || Function.prototype;
+ this._subscriptions = options.subscriptions || Function.prototype;
+ this._triggersEnter = options.triggersEnter || [];
+ this._triggersExit = options.triggersExit || [];
+ this._subsMap = {};
+ this._router = router;
+
+ this._params = new ReactiveDict();
+ this._queryParams = new ReactiveDict();
+ this._routeCloseDep = new Tracker.Dependency();
+
+ // tracks the changes in the URL
+ this._pathChangeDep = new Tracker.Dependency();
+
+ this.group = group;
+};
+
+Route.prototype.clearSubscriptions = function() {
+ this._subsMap = {};
+};
+
+Route.prototype.register = function(name, sub, options) {
+ this._subsMap[name] = sub;
+};
+
+
+Route.prototype.getSubscription = function(name) {
+ return this._subsMap[name];
+};
+
+
+Route.prototype.getAllSubscriptions = function() {
+ return this._subsMap;
+};
+
+Route.prototype.callAction = function(current) {
+ var self = this;
+ self._action(current.params, current.queryParams);
+};
+
+Route.prototype.callSubscriptions = function(current) {
+ this.clearSubscriptions();
+ if (this.group) {
+ this.group.callSubscriptions(current);
+ }
+
+ this._subscriptions(current.params, current.queryParams);
+};
+
+Route.prototype.getRouteName = function() {
+ this._routeCloseDep.depend();
+ return this.name;
+};
+
+Route.prototype.getParam = function(key) {
+ this._routeCloseDep.depend();
+ return this._params.get(key);
+};
+
+Route.prototype.getQueryParam = function(key) {
+ this._routeCloseDep.depend();
+ return this._queryParams.get(key);
+};
+
+Route.prototype.watchPathChange = function() {
+ this._pathChangeDep.depend();
+};
+
+Route.prototype.registerRouteClose = function() {
+ this._params = new ReactiveDict();
+ this._queryParams = new ReactiveDict();
+ this._routeCloseDep.changed();
+ this._pathChangeDep.changed();
+};
+
+Route.prototype.registerRouteChange = function(currentContext, routeChanging) {
+ // register params
+ var params = currentContext.params;
+ this._updateReactiveDict(this._params, params);
+
+ // register query params
+ var queryParams = currentContext.queryParams;
+ this._updateReactiveDict(this._queryParams, queryParams);
+
+ // if the route is changing, we need to defer triggering path changing
+ // if we did this, old route's path watchers will detect this
+ // Real issue is, above watcher will get removed with the new route
+ // So, we don't need to trigger it now
+ // We are doing it on the route close event. So, if they exists they'll
+ // get notify that
+ if(!routeChanging) {
+ this._pathChangeDep.changed();
+ }
+};
+
+Route.prototype._updateReactiveDict = function(dict, newValues) {
+ var currentKeys = _.keys(newValues);
+ var oldKeys = _.keys(dict.keyDeps);
+
+ // set new values
+ // params is an array. So, _.each(params) does not works
+ // to iterate params
+ _.each(currentKeys, function(key) {
+ dict.set(key, newValues[key]);
+ });
+
+ // remove keys which does not exisits here
+ var removedKeys = _.difference(oldKeys, currentKeys);
+ _.each(removedKeys, function(key) {
+ dict.set(key, undefined);
+ });
+};
diff --git a/packages/kadira-flow-router/client/router.js b/packages/kadira-flow-router/client/router.js
new file mode 100644
index 00000000..ae91751f
--- /dev/null
+++ b/packages/kadira-flow-router/client/router.js
@@ -0,0 +1,587 @@
+Router = function () {
+ var self = this;
+ this.globals = [];
+ this.subscriptions = Function.prototype;
+
+ this._tracker = this._buildTracker();
+ this._current = {};
+
+ // tracks the current path change
+ this._onEveryPath = new Tracker.Dependency();
+
+ this._globalRoute = new Route(this);
+
+ // holds onRoute callbacks
+ this._onRouteCallbacks = [];
+
+ // if _askedToWait is true. We don't automatically start the router
+ // in Meteor.startup callback. (see client/_init.js)
+ // Instead user need to call `.initialize()
+ this._askedToWait = false;
+ this._initialized = false;
+ this._triggersEnter = [];
+ this._triggersExit = [];
+ this._routes = [];
+ this._routesMap = {};
+ this._updateCallbacks();
+ this.notFound = this.notfound = null;
+ // indicate it's okay (or not okay) to run the tracker
+ // when doing subscriptions
+ // using a number and increment it help us to support FlowRouter.go()
+ // and legitimate reruns inside tracker on the same event loop.
+ // this is a solution for #145
+ this.safeToRun = 0;
+
+ // Meteor exposes to the client the path prefix that was defined using the
+ // ROOT_URL environement variable on the server using the global runtime
+ // configuration. See #315.
+ this._basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
+
+ // this is a chain contains a list of old routes
+ // most of the time, there is only one old route
+ // but when it's the time for a trigger redirect we've a chain
+ this._oldRouteChain = [];
+
+ this.env = {
+ replaceState: new Meteor.EnvironmentVariable(),
+ reload: new Meteor.EnvironmentVariable(),
+ trailingSlash: new Meteor.EnvironmentVariable()
+ };
+
+ // redirect function used inside triggers
+ this._redirectFn = function(pathDef, fields, queryParams) {
+ if (/^http(s)?:\/\//.test(pathDef)) {
+ var message = "Redirects to URLs outside of the app are not supported in this version of Flow Router. Use 'window.location = yourUrl' instead";
+ throw new Error(message);
+ }
+ self.withReplaceState(function() {
+ var path = FlowRouter.path(pathDef, fields, queryParams);
+ self._page.redirect(path);
+ });
+ };
+ this._initTriggersAPI();
+};
+
+Router.prototype.route = function(pathDef, options, group) {
+ if (!/^\/.*/.test(pathDef)) {
+ var message = "route's path must start with '/'";
+ throw new Error(message);
+ }
+
+ options = options || {};
+ var self = this;
+ var route = new Route(this, pathDef, options, group);
+
+ // calls when the page route being activates
+ route._actionHandle = function (context, next) {
+ var oldRoute = self._current.route;
+ self._oldRouteChain.push(oldRoute);
+
+ var queryParams = self._qs.parse(context.querystring);
+ // _qs.parse() gives us a object without prototypes,
+ // created with Object.create(null)
+ // Meteor's check doesn't play nice with it.
+ // So, we need to fix it by cloning it.
+ // see more: https://github.com/meteorhacks/flow-router/issues/164
+ queryParams = JSON.parse(JSON.stringify(queryParams));
+
+ self._current = {
+ path: context.path,
+ context: context,
+ params: context.params,
+ queryParams: queryParams,
+ route: route,
+ oldRoute: oldRoute
+ };
+
+ // 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);
+ if (options.name) {
+ this._routesMap[options.name] = route;
+ }
+
+ this._updateCallbacks();
+ this._triggerRouteRegister(route);
+
+ return route;
+};
+
+Router.prototype.group = function(options) {
+ return new Group(this, options);
+};
+
+Router.prototype.path = function(pathDef, fields, queryParams) {
+ if (this._routesMap[pathDef]) {
+ pathDef = this._routesMap[pathDef].pathDef;
+ }
+
+ var path = "";
+
+ // Prefix the path with the router global prefix
+ if (this._basePath) {
+ path += "/" + this._basePath + "/";
+ }
+
+ fields = fields || {};
+ var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g;
+ path += pathDef.replace(regExp, function(key) {
+ var firstRegexpChar = key.indexOf("(");
+ // get the content behind : and (\\d+/)
+ key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined);
+ // remove +?*
+ key = key.replace(/[\+\*\?]+/g, "");
+
+ // this is to allow page js to keep the custom characters as it is
+ // we need to encode 2 times otherwise "/" char does not work properly
+ // So, in that case, when I includes "/" it will think it's a part of the
+ // route. encoding 2times fixes it
+ return encodeURIComponent(encodeURIComponent(fields[key] || ""));
+ });
+
+ // Replace multiple slashes with single slash
+ path = path.replace(/\/\/+/g, "/");
+
+ // remove trailing slash
+ // but keep the root slash if it's the only one
+ path = path.match(/^\/{1}$/) ? path: path.replace(/\/$/, "");
+
+ // explictly asked to add a trailing slash
+ if(this.env.trailingSlash.get() && _.last(path) !== "/") {
+ path += "/";
+ }
+
+ var strQueryParams = this._qs.stringify(queryParams || {});
+ if(strQueryParams) {
+ path += "?" + strQueryParams;
+ }
+
+ return path;
+};
+
+Router.prototype.go = function(pathDef, fields, queryParams) {
+ var path = this.path(pathDef, fields, queryParams);
+
+ var useReplaceState = this.env.replaceState.get();
+ if(useReplaceState) {
+ this._page.replace(path);
+ } else {
+ this._page(path);
+ }
+};
+
+Router.prototype.reload = function() {
+ var self = this;
+
+ self.env.reload.withValue(true, function() {
+ self._page.replace(self._current.path);
+ });
+};
+
+Router.prototype.redirect = function(path) {
+ this._page.redirect(path);
+};
+
+Router.prototype.setParams = function(newParams) {
+ if(!this._current.route) {return false;}
+
+ var pathDef = this._current.route.pathDef;
+ var existingParams = this._current.params;
+ var params = {};
+ _.each(_.keys(existingParams), function(key) {
+ params[key] = existingParams[key];
+ });
+
+ params = _.extend(params, newParams);
+ var queryParams = this._current.queryParams;
+
+ this.go(pathDef, params, queryParams);
+ return true;
+};
+
+Router.prototype.setQueryParams = function(newParams) {
+ if(!this._current.route) {return false;}
+
+ var queryParams = _.clone(this._current.queryParams);
+ _.extend(queryParams, newParams);
+
+ for (var k in queryParams) {
+ if (queryParams[k] === null || queryParams[k] === undefined) {
+ delete queryParams[k];
+ }
+ }
+
+ var pathDef = this._current.route.pathDef;
+ var params = this._current.params;
+ this.go(pathDef, params, queryParams);
+ return true;
+};
+
+// .current is not reactive
+// This is by design. use .getParam() instead
+// If you really need to watch the path change, use .watchPathChange()
+Router.prototype.current = function() {
+ // We can't trust outside, that's why we clone this
+ // Anyway, we can't clone the whole object since it has non-jsonable values
+ // That's why we clone what's really needed.
+ var current = _.clone(this._current);
+ current.queryParams = EJSON.clone(current.queryParams);
+ current.params = EJSON.clone(current.params);
+ return current;
+};
+
+// Implementing Reactive APIs
+var reactiveApis = [
+ 'getParam', 'getQueryParam',
+ 'getRouteName', 'watchPathChange'
+];
+reactiveApis.forEach(function(api) {
+ Router.prototype[api] = function(arg1) {
+ // when this is calling, there may not be any route initiated
+ // so we need to handle it
+ var currentRoute = this._current.route;
+ if(!currentRoute) {
+ this._onEveryPath.depend();
+ return;
+ }
+
+ // currently, there is only one argument. If we've more let's add more args
+ // this is not clean code, but better in performance
+ return currentRoute[api].call(currentRoute, arg1);
+ };
+});
+
+Router.prototype.subsReady = function() {
+ var callback = null;
+ var args = _.toArray(arguments);
+
+ if (typeof _.last(args) === "function") {
+ callback = args.pop();
+ }
+
+ var currentRoute = this.current().route;
+ var globalRoute = this._globalRoute;
+
+ // we need to depend for every route change and
+ // rerun subscriptions to check the ready state
+ this._onEveryPath.depend();
+
+ if(!currentRoute) {
+ return false;
+ }
+
+ var subscriptions;
+ if(args.length === 0) {
+ subscriptions = _.values(globalRoute.getAllSubscriptions());
+ subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions()));
+ } else {
+ subscriptions = _.map(args, function(subName) {
+ return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName);
+ });
+ }
+
+ var isReady = function() {
+ var ready = _.every(subscriptions, function(sub) {
+ return sub && sub.ready();
+ });
+
+ return ready;
+ };
+
+ if (callback) {
+ Tracker.autorun(function(c) {
+ if (isReady()) {
+ callback();
+ c.stop();
+ }
+ });
+ } else {
+ return isReady();
+ }
+};
+
+Router.prototype.withReplaceState = function(fn) {
+ return this.env.replaceState.withValue(true, fn);
+};
+
+Router.prototype.withTrailingSlash = function(fn) {
+ return this.env.trailingSlash.withValue(true, fn);
+};
+
+Router.prototype._notfoundRoute = function(context) {
+ this._current = {
+ path: context.path,
+ context: context,
+ params: [],
+ queryParams: {},
+ };
+
+ // XXX this.notfound kept for backwards compatibility
+ this.notFound = this.notFound || this.notfound;
+ if(!this.notFound) {
+ console.error("There is no route for the path:", context.path);
+ return;
+ }
+
+ this._current.route = new Route(this, "*", this.notFound);
+ this._invalidateTracker();
+};
+
+Router.prototype.initialize = function(options) {
+ options = options || {};
+
+ if(this._initialized) {
+ throw new Error("FlowRouter is already initialized");
+ }
+
+ var self = this;
+ this._updateCallbacks();
+
+ // Implementing idempotent routing
+ // by overriding page.js`s "show" method.
+ // Why?
+ // It is impossible to bypass exit triggers,
+ // because they execute before the handler and
+ // can not know what the next path is, inside exit trigger.
+ //
+ // we need override both show, replace to make this work
+ // since we use redirect when we are talking about withReplaceState
+ _.each(['show', 'replace'], function(fnName) {
+ var original = self._page[fnName];
+ self._page[fnName] = function(path, state, dispatch, push) {
+ var reload = self.env.reload.get();
+ if (!reload && self._current.path === path) {
+ return;
+ }
+
+ original.call(this, path, state, dispatch, push);
+ };
+ });
+
+ // this is very ugly part of pagejs and it does decoding few times
+ // in unpredicatable manner. See #168
+ // this is the default behaviour and we need keep it like that
+ // we are doing a hack. see .path()
+ this._page.base(this._basePath);
+ this._page({
+ decodeURLComponents: true,
+ hashbang: !!options.hashbang
+ });
+
+ this._initialized = true;
+};
+
+Router.prototype._buildTracker = function() {
+ var self = this;
+
+ // main autorun function
+ var tracker = Tracker.autorun(function () {
+ if(!self._current || !self._current.route) {
+ return;
+ }
+
+ // see the definition of `this._processingContexts`
+ var currentContext = self._current;
+ var route = currentContext.route;
+ var path = currentContext.path;
+
+ if(self.safeToRun === 0) {
+ var message =
+ "You can't use reactive data sources like Session" +
+ " inside the `.subscriptions` method!";
+ throw new Error(message);
+ }
+
+ // We need to run subscriptions inside a Tracker
+ // to stop subs when switching between routes
+ // But we don't need to run this tracker with
+ // other reactive changes inside the .subscription method
+ // We tackle this with the `safeToRun` variable
+ self._globalRoute.clearSubscriptions();
+ self.subscriptions.call(self._globalRoute, path);
+ route.callSubscriptions(currentContext);
+
+ // otherwise, computations inside action will trigger to re-run
+ // this computation. which we do not need.
+ Tracker.nonreactive(function() {
+ var isRouteChange = currentContext.oldRoute !== currentContext.route;
+ var isFirstRoute = !currentContext.oldRoute;
+ // first route is not a route change
+ if(isFirstRoute) {
+ isRouteChange = false;
+ }
+
+ // Clear oldRouteChain just before calling the action
+ // We still need to get a copy of the oldestRoute first
+ // It's very important to get the oldest route and registerRouteClose() it
+ // See: https://github.com/kadirahq/flow-router/issues/314
+ var oldestRoute = self._oldRouteChain[0];
+ self._oldRouteChain = [];
+
+ currentContext.route.registerRouteChange(currentContext, isRouteChange);
+ route.callAction(currentContext);
+
+ Tracker.afterFlush(function() {
+ self._onEveryPath.changed();
+ if(isRouteChange) {
+ // We need to trigger that route (definition itself) has changed.
+ // So, we need to re-run all the register callbacks to current route
+ // This is pretty important, otherwise tracker
+ // can't identify new route's items
+
+ // We also need to afterFlush, otherwise this will re-run
+ // helpers on templates which are marked for destroying
+ if(oldestRoute) {
+ oldestRoute.registerRouteClose();
+ }
+ }
+ });
+ });
+
+ self.safeToRun--;
+ });
+
+ return tracker;
+};
+
+Router.prototype._invalidateTracker = function() {
+ var self = this;
+ this.safeToRun++;
+ this._tracker.invalidate();
+ // After the invalidation we need to flush to make changes imediately
+ // otherwise, we have face some issues context mix-maches and so on.
+ // But there are some cases we can't flush. So we need to ready for that.
+
+ // we clearly know, we can't flush inside an autorun
+ // this may leads some issues on flow-routing
+ // we may need to do some warning
+ if(!Tracker.currentComputation) {
+ // Still there are some cases where we can't flush
+ // eg:- when there is a flush currently
+ // But we've no public API or hacks to get that state
+ // So, this is the only solution
+ try {
+ Tracker.flush();
+ } catch(ex) {
+ // only handling "while flushing" errors
+ if(!/Tracker\.flush while flushing/.test(ex.message)) {
+ return;
+ }
+
+ // XXX: fix this with a proper solution by removing subscription mgt.
+ // from the router. Then we don't need to run invalidate using a tracker
+
+ // this happens when we are trying to invoke a route change
+ // with inside a route chnage. (eg:- Template.onCreated)
+ // Since we use page.js and tracker, we don't have much control
+ // over this process.
+ // only solution is to defer route execution.
+
+ // It's possible to have more than one path want to defer
+ // But, we only need to pick the last one.
+ // self._nextPath = self._current.path;
+ Meteor.defer(function() {
+ var path = self._nextPath;
+ if(!path) {
+ return;
+ }
+
+ delete self._nextPath;
+ self.env.reload.withValue(true, function() {
+ self.go(path);
+ });
+ });
+ }
+ }
+};
+
+Router.prototype._updateCallbacks = function () {
+ var self = this;
+
+ self._page.callbacks = [];
+ self._page.exits = [];
+
+ _.each(self._routes, function(route) {
+ self._page(route.pathDef, route._actionHandle);
+ self._page.exit(route.pathDef, route._exitHandle);
+ });
+
+ 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);
+ if(triggers.length) {
+ self._triggersEnter = self._triggersEnter.concat(triggers);
+ }
+ },
+
+ exit: function(triggers, filter) {
+ triggers = Triggers.applyFilters(triggers, filter);
+ if(triggers.length) {
+ self._triggersExit = self._triggersExit.concat(triggers);
+ }
+ }
+ };
+};
+
+Router.prototype.wait = function() {
+ if(this._initialized) {
+ throw new Error("can't wait after FlowRouter has been initialized");
+ }
+
+ this._askedToWait = true;
+};
+
+Router.prototype.onRouteRegister = function(cb) {
+ this._onRouteCallbacks.push(cb);
+};
+
+Router.prototype._triggerRouteRegister = function(currentRoute) {
+ // We should only need to send a safe set of fields on the route
+ // object.
+ // This is not to hide what's inside the route object, but to show
+ // these are the public APIs
+ var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path');
+ var omittingOptionFields = [
+ 'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name'
+ ];
+ routePublicApi.options = _.omit(currentRoute.options, omittingOptionFields);
+
+ _.each(this._onRouteCallbacks, function(cb) {
+ cb(routePublicApi);
+ });
+};
+
+Router.prototype._page = page;
+Router.prototype._qs = qs;
diff --git a/packages/kadira-flow-router/client/triggers.js b/packages/kadira-flow-router/client/triggers.js
new file mode 100644
index 00000000..7733332c
--- /dev/null
+++ b/packages/kadira-flow-router/client/triggers.js
@@ -0,0 +1,112 @@
+// 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 or callback stopped
+// @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, doStop);
+
+ if(abort) {
+ return;
+ }
+ }
+
+ // mark that, we've exceeds the currentEventloop for
+ // this set of triggers.
+ inCurrentLoop = false;
+ after();
+
+ function doRedirect(url, params, queryParams) {
+ 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, params, queryParams);
+ }
+
+ function doStop() {
+ abort = true;
+ }
+}; \ No newline at end of file