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

Support prefixed paths #329

Merged
merged 5 commits into from
Oct 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ It exposes a great API for changing the URL and reactively getting data from the
* [Subscription Management](#subscription-management)
* [IE9 Support](#ie9-support)
* [Hashbang URLs](#hashbang-urls)
* [Prefixed paths](#prefixed-paths)
* [Addons](#addons)
* [Difference with Iron Router](#difference-with-iron-router)
* [Migrating into 2.0](#migrating-into-20)
Expand Down Expand Up @@ -637,6 +638,9 @@ WhenEverYourAppIsReady(function() {
});
~~~
## Prefixed paths
In cases you wish to run multiple web application on the same domain name, you’ll probably want to serve your particular meteor application under a sub-path (eg `example.com/myapp`). In this case simply include the path prefix in the meteor `ROOT_URL` environment variable and FlowRouter will handle it transparently without any additional configuration.
## Addons
Expand Down
56 changes: 37 additions & 19 deletions client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Router = function () {
// holds onRoute callbacks
this._onRouteCallbacks = [];

// if _askedToWait is true. We don't automatically start the router
// 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;
Expand All @@ -27,11 +27,16 @@ Router = function () {
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()
// 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
Expand Down Expand Up @@ -69,9 +74,9 @@ Router.prototype.route = function(pathDef, options, group) {
self._oldRouteChain.push(oldRoute);

var queryParams = self._qs.parse(context.querystring);
// _qs.parse() gives us a object without prototypes,
// _qs.parse() gives us a object without prototypes,
// created with Object.create(null)
// Meteor's check doesn't play nice with it.
// 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));
Expand All @@ -94,9 +99,9 @@ Router.prototype.route = function(pathDef, options, group) {

var triggers = self._triggersEnter.concat(route._triggersEnter);
Triggers.runTriggers(
triggers,
self._current,
self._redirectFn,
triggers,
self._current,
self._redirectFn,
afterAllTriggersRan
);
};
Expand Down Expand Up @@ -132,9 +137,16 @@ Router.prototype.path = function(pathDef, fields, queryParams) {
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;
var path = pathDef.replace(regExp, function(key) {
path += pathDef.replace(regExp, function(key) {
var firstRegexpChar = key.indexOf("(");
// get the content behind : and (\\d+/)
key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined);
Expand All @@ -143,12 +155,13 @@ Router.prototype.path = function(pathDef, fields, queryParams) {

// 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
// 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] || ""));
});

path = path.replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash
// Replace multiple slashes with single slash
path = path.replace(/\/\/+/g, "/");

// remove trailing slash
// but keep the root slash if it's the only one
Expand All @@ -169,7 +182,7 @@ Router.prototype.path = function(pathDef, fields, queryParams) {

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);
Expand Down Expand Up @@ -234,7 +247,7 @@ Router.prototype.current = function() {

// Implementing Reactive APIs
var reactiveApis = [
'getParam', 'getQueryParam',
'getParam', 'getQueryParam',
'getRouteName', 'watchPathChange'
];
reactiveApis.forEach(function(api) {
Expand Down Expand Up @@ -343,11 +356,11 @@ Router.prototype.initialize = function(options) {
// by overriding page.js`s "show" method.
// Why?
// It is impossible to bypass exit triggers,
// becuase they execute before the handler and
// 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
// 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) {
Expand All @@ -364,7 +377,12 @@ Router.prototype.initialize = function(options) {
// 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({decodeURLComponents: true, hashbang: !!options.hashbang});
this._page.base(this._basePath);
this._page({
decodeURLComponents: true,
hashbang: !!options.hashbang
});

this._initialized = true;
};

Expand Down Expand Up @@ -423,7 +441,7 @@ Router.prototype._buildTracker = function() {
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
// This is pretty important, otherwise tracker
// can't identify new route's items

// We also need to afterFlush, otherwise this will re-run
Expand Down Expand Up @@ -468,9 +486,9 @@ Router.prototype._invalidateTracker = function() {
// 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
// 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
// Since we use page.js and tracker, we don't have much control
// over this process.
// only solution is to defer route execution.

Expand Down Expand Up @@ -542,7 +560,7 @@ Router.prototype.onRouteRegister = function(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
// 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 = [
Expand Down
67 changes: 61 additions & 6 deletions test/client/router.core.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ Tinytest.addAsync('Client - Router - notFound', function (test, done) {
}, 50);
});

Tinytest.addAsync('Client - Router - withReplaceState - enabled',
Tinytest.addAsync('Client - Router - withReplaceState - enabled',
function (test, done) {
var pathDef = "/" + Random.id() + "/:id";
var originalRedirect = FlowRouter._page.replace;
Expand All @@ -362,7 +362,7 @@ function (test, done) {
test.equal(params.id, "awesome");
test.equal(callCount, 1);
FlowRouter._page.replace = originalRedirect;
// We don't use Meteor.defer here since it carries
// We don't use Meteor.defer here since it carries
// Meteor.Environment vars too
// Which breaks our test below
setTimeout(done, 0);
Expand All @@ -374,7 +374,7 @@ function (test, done) {
});
});

Tinytest.addAsync('Client - Router - withReplaceState - disabled',
Tinytest.addAsync('Client - Router - withReplaceState - disabled',
function (test, done) {
var pathDef = "/" + Random.id() + "/:id";
var originalRedirect = FlowRouter._page.replace;
Expand Down Expand Up @@ -537,7 +537,7 @@ function (test, next) {
});

Tinytest.addAsync(
'Client - Router - wait - before initialize',
'Client - Router - wait - before initialize',
function(test, done) {
FlowRouter._initialized = false;
FlowRouter.wait();
Expand All @@ -549,7 +549,7 @@ function(test, done) {
});

Tinytest.addAsync(
'Client - Router - wait - after initialized',
'Client - Router - wait - after initialized',
function(test, done) {
try {
FlowRouter.wait();
Expand All @@ -560,7 +560,7 @@ function(test, done) {
});

Tinytest.addAsync(
'Client - Router - initialize - after initialized',
'Client - Router - initialize - after initialized',
function(test, done) {
try {
FlowRouter.initialize();
Expand All @@ -570,6 +570,61 @@ function(test, done) {
}
});

Tinytest.addAsync(
'Client - Router - base path - url updated',
function(test, done) {
var simulatedBasePath = '/flow';
var rand = Random.id();
FlowRouter.route('/' + rand, { action: function() {} });

setBasePath(simulatedBasePath);
FlowRouter.go('/' + rand);
setTimeout(function() {
test.equal(location.pathname, simulatedBasePath + '/' + rand);
resetBasePath();
done();
}, 100);
});

Tinytest.addAsync(
'Client - Router - base path - route action called',
function(test, done) {
var simulatedBasePath = '/flow';
var rand = Random.id();
FlowRouter.route('/' + rand, {
action: function() {
resetBasePath();
done();
}
});

setBasePath(simulatedBasePath);
FlowRouter.go('/' + rand);
});

Tinytest.add(
'Client - Router - base path - path generation',
function(test, done) {
_.each(['/flow', '/flow/', 'flow/', 'flow'], function(simulatedBasePath) {
var rand = Random.id();
setBasePath(simulatedBasePath);
test.equal(FlowRouter.path('/' + rand), '/flow/' + rand);
});
resetBasePath();
});


function setBasePath(path) {
FlowRouter._initialized = false;
FlowRouter._basePath = path;
FlowRouter.initialize();
}

var defaultBasePath = FlowRouter._basePath;
function resetBasePath() {
setBasePath(defaultBasePath);
}

function bind(obj, method) {
return function() {
obj[method].apply(obj, arguments);
Expand Down