/**
* @summary router
* @version 3.0.0
* @since 1.0.0
* @author Arian Khosravi <arian.khosravi@aofl.com>
*/
import PathUtils from '../path-utils';
import {Middleware} from '@aofl/middleware';
import matchRouteMiddleware from '../match-route-middleware';
import {redirectMiddleware} from '../redirect-middleware';
/**
* A client side router that uses history api and implements a middleware. This router
* implementation is minimal yet very powerful. It will match routes and handles
* redirects out of the box. Everything else can be programmed using an appropriate
* middleware function.
*
* @memberof module:@aofl/router
*/
class Router {
/**
* Create an instance of Router
*/
constructor() {
this.removeMatchRouteMiddleware = /* istanbul ignore next */ () => {};
this.removeRedirectMiddleware = /* istanbul ignore next */ () => {};
this.removeListener = /* istanbul ignore next */ () => {};
Object.defineProperties(this, {
middleware: {
value: new Middleware('before', 'after', 'afterEach', 'beforeEach')
},
resolve: {
writable: true
},
currentRoute: {
writable: true
}
});
}
/**
* Loads rotes config
* @param {Array} config
* @return {void}
*/
init(config) {
this.config = {
routes: this.addRegexRoutes(config)
};
this.removeMatchRouteMiddleware = this.beforeEach(matchRouteMiddleware(this));
this.removeRedirectMiddleware = this.afterEach(redirectMiddleware(this));
this.removeListener = this.listen();
}
/**
*
* @param {Function} fn
* @return {void}
*/
before(fn) {
return this.middleware.use(fn, 'before');
}
/**
*
* @param {Function} fn
* @return {void}
*/
after(fn) {
return this.middleware.use(fn, 'after');
}
/**
* Registers a post middleware function
* @param {Function} fn
* @return {void}
*/
afterEach(fn) {
return this.middleware.use(fn, 'afterEach');
}
/**
* Registers a pre middle function
* @param {Function} fn
* @return {void}
*/
beforeEach(fn) {
return this.middleware.use(fn, 'beforeEach');
}
/**
* Runs all middleware
* @private
* @param {Object} request
*/
async applyMiddleware(request) {
const beforeEachResponse = await this.middleware.iterateMiddleware(request, 'beforeEach', Object.assign({}, request));
await this.middleware.iterateMiddleware(request, 'afterEach', Object.assign({}, beforeEachResponse));
const afterResponse = await this.middleware.iterateMiddleware(request, 'after', Object.assign({}, beforeEachResponse));
if (!request.meta.poppedState) {
if (request.meta.replaceState) {
window.history.replaceState(null, null, afterResponse.to);
} else {
window.history.pushState(null, null, afterResponse.to);
}
}
this.resolve();
}
/**
* Adds regex version of routes for dynamic routing
* @private
* @param {Object} routes
* @return {Object}
*/
addRegexRoutes(routes) {
for (let i = 0; i < routes.length; i++) {
const {regex, parse} = PathUtils.getRegex(routes[i].path);
routes[i] = Object.assign({}, routes[i], {
regex,
parse
});
}
return routes;
}
/**
* Listens to changes on history
* @private
* @return {Function}
*/
listen() {
const popStateHandler = (e) => {
e.preventDefault();
this.navigate(location.href.replace(location.origin, ''), {
forceReload: true,
poppedState: true
});
};
window.addEventListener('popstate', popStateHandler);
return () => {
window.removeEventListener('popstate', popStateHandler);
};
}
/**
* public method which attempts to load the given path
* @param {String} path
* @param {Object} _meta
* @return {Promise}
*/
navigate(path, _meta) {
const meta = Object.assign({poppedState: false, forceReload: false, replaceState: false}, _meta);
return new Promise((resolve, reject) => {
this.resolve = resolve;
const request = {
to: path,
from: this.currentRoute? this.currentRoute.to: document.referrer,
routes: this.config.routes,
meta
};
if (path !== location.href.replace(location.origin, '') || meta.forceReload) {
this.middleware.iterateMiddleware(request, 'before', Object.assign({}, request))
.then(() => {
this.applyMiddleware(request);
});
} else {
reject('Can\'t navigate to current path');
}
});
}
}
export default Router;