frontexpress/lib/application.js

387 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2016-07-15 19:34:24 +00:00
/**
* Module dependencies.
* @private
*/
2016-07-14 13:14:16 +00:00
import HTTP_METHODS from './methods';
import Settings from './settings';
import Router, {Route} from './router';
import Middleware from './middleware';
2016-07-15 19:34:24 +00:00
/**
* Application class.
*/
export default class Application {
2016-07-15 19:34:24 +00:00
/**
* Initialize the application.
*
* - setup default configuration
*
* @private
*/
constructor() {
this.routers = [];
2017-01-15 11:00:51 +00:00
// this.isDOMLoaded = false;
// this.isDOMReady = false;
2016-07-14 13:14:16 +00:00
this.settings = new Settings();
}
2016-07-15 19:34:24 +00:00
/**
* Assign `setting` to `val`, or return `setting`'s value.
*
* app.set('foo', 'bar');
* app.set('foo');
* // => "bar"
*
* @param {String} setting
* @param {*} [val]
* @return {app} for chaining
* @public
*/
set(...args) {
// get behaviour
if (args.length === 1) {
return this.settings.get([args]);
2016-07-15 19:34:24 +00:00
}
// set behaviour
const [name, value] = args;
2016-07-14 13:14:16 +00:00
this.settings.set(name, value);
2016-07-15 19:34:24 +00:00
return this;
}
2016-07-15 19:34:24 +00:00
/**
* Listen for DOM initialization and history state changes.
*
* The callback function is called once the DOM has
* the `document.readyState` equals to 'interactive'.
*
* app.listen(()=> {
* console.log('App is listening requests');
* console.log('DOM is ready!');
* });
*
*
* @param {Function} callback
* @public
*/
listen(callback) {
2017-01-15 11:00:51 +00:00
// manage history
window.onpopstate = (event) => {
if (event.state) {
2016-07-23 10:31:40 +00:00
const {request, response} = event.state;
const currentRoutes = this._routes(request.uri, request.method);
this._callMiddlewareMethod('exited');
this._callMiddlewareMethod('entered', currentRoutes, request);
this._callMiddlewareMethod('updated', currentRoutes, request, response);
}
};
2017-01-15 11:00:51 +00:00
// manage page loading/refreshing
const request = {method: 'GET', uri: window.location.pathname + window.location.search};
const response = {status: 200, statusText: 'OK'};
const currentRoutes = this._routes();
const whenPageIsInteractiveFn = () => {
this._callMiddlewareMethod('updated', currentRoutes, request, response);
if (callback) {
callback(request, response);
}
};
window.onbeforeunload = () => {
this._callMiddlewareMethod('exited');
};
this._callMiddlewareMethod('entered', currentRoutes, request);
document.onreadystatechange = () => {
2017-01-15 11:00:51 +00:00
// DOM ready state
if (document.readyState === 'interactive') {
2017-01-15 11:00:51 +00:00
whenPageIsInteractiveFn();
}
};
2017-01-15 11:00:51 +00:00
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
whenPageIsInteractiveFn();
}
}
2016-07-15 19:34:24 +00:00
/**
* Returns a new `Router` instance for the _uri_.
* See the Router api docs for details.
*
* app.route('/');
* // => new Router instance
*
* @param {String} uri
* @return {Router} for chaining
*
* @public
*/
route(uri) {
const router = new Router(uri);
this.routers.push(router);
return router;
}
2016-07-15 19:34:24 +00:00
/**
* Use the given middleware function or object, with optional _uri_.
* Default _uri_ is "/".
*
* // middleware function will be applied on path "/"
* app.use((req, res, next) => {console.log('Hello')});
*
* // middleware object will be applied on path "/"
* app.use(new Middleware());
*
* @param {String} uri
* @param {Middleware|Function} middleware object or function
2016-07-15 19:34:24 +00:00
* @return {app} for chaining
*
* @public
*/
use(...args) {
2017-01-14 14:02:50 +00:00
let {baseUri, router, middleware} = toParameters(args);
if (router) {
router.baseUri = baseUri;
2017-01-14 14:02:50 +00:00
} else if (middleware) {
router = new Router(baseUri);
2017-01-15 11:00:51 +00:00
HTTP_METHODS.forEach((method) => {
router[method.toLowerCase()](middleware);
2017-01-15 11:00:51 +00:00
});
} else {
2017-01-14 14:02:50 +00:00
throw new TypeError('method takes at least a middleware or a router');
}
this.routers.push(router);
2016-07-15 19:34:24 +00:00
return this;
}
2016-07-15 19:34:24 +00:00
/**
* Gather routes from all routers filtered by _uri_ and HTTP _method_.
* See Router#routes() documentation for details.
*
* @private
*/
_routes(uri=window.location.pathname + window.location.search, method='GET') {
const currentRoutes = [];
2017-01-15 11:00:51 +00:00
this.routers.forEach((router) => {
currentRoutes.push(...router.routes(uri, method));
2017-01-15 11:00:51 +00:00
});
return currentRoutes;
}
2016-07-15 19:34:24 +00:00
/**
* Call `Middleware` method or middleware function on _currentRoutes_.
2016-07-15 19:34:24 +00:00
*
* @private
*/
_callMiddlewareMethod(meth, currentRoutes, request, response) {
if (meth === 'exited') {
// currentRoutes, request, response params not needed
2017-01-15 11:00:51 +00:00
this.routers.forEach((router) => {
router.visited().forEach((route) => {
if (route.middleware.exited) {
route.middleware.exited(route.visited);
route.visited = null;
}
2017-01-15 11:00:51 +00:00
});
});
return;
}
2016-07-15 19:34:24 +00:00
2017-01-15 11:00:51 +00:00
currentRoutes.some((route) => {
if (meth === 'updated') {
route.visited = request;
}
2016-07-15 19:34:24 +00:00
if (route.middleware[meth]) {
route.middleware[meth](request, response);
if (route.middleware.next && !route.middleware.next()) {
2017-01-15 11:00:51 +00:00
return true;
}
} else if (meth !== 'entered') {
// calls middleware method
let breakMiddlewareLoop = true;
const next = () => {
breakMiddlewareLoop = false;
};
route.middleware(request, response, next);
if (breakMiddlewareLoop) {
2017-01-15 11:00:51 +00:00
return true;
}
}
2017-01-15 11:00:51 +00:00
return false;
});
}
2016-07-15 19:34:24 +00:00
/**
* Make an ajax request. Manage History#pushState if history object set.
2016-07-15 19:34:24 +00:00
*
* @private
*/
2017-01-15 11:00:51 +00:00
_fetch(req, resolve, reject) {
let {method, uri, headers, data, history} = req;
2016-07-23 10:31:40 +00:00
2016-07-14 13:14:16 +00:00
const httpMethodTransformer = this.get(`http ${method} transformer`);
if (httpMethodTransformer) {
2017-01-15 11:00:51 +00:00
const {uri: _uriFn, headers: _headersFn, data: _dataFn } = httpMethodTransformer;
req.uri = _uriFn ? _uriFn({uri, headers, data}) : uri;
req.headers = _headersFn ? _headersFn({uri, headers, data}) : headers;
req.data = _dataFn ? _dataFn({uri, headers, data}) : data;
2016-07-14 13:14:16 +00:00
}
// calls middleware exited method
this._callMiddlewareMethod('exited');
// gathers all routes impacted by the uri
const currentRoutes = this._routes(uri, method);
// calls middleware entered method
2017-01-15 11:00:51 +00:00
this._callMiddlewareMethod('entered', currentRoutes, req);
// invokes http request
2017-01-15 11:00:51 +00:00
this.settings.get('http requester').fetch(req,
(request, response) => {
if (history) {
2017-01-15 11:00:51 +00:00
window.history.pushState({request, response}, history.title, history.uri);
}
2017-01-15 11:00:51 +00:00
this._callMiddlewareMethod('updated', currentRoutes, request, response);
if (resolve) {
2017-01-15 11:00:51 +00:00
resolve(request, response);
}
},
2017-01-15 11:00:51 +00:00
(request, response) => {
this._callMiddlewareMethod('failed', currentRoutes, request, response);
if (reject) {
2017-01-15 11:00:51 +00:00
reject(request, response);
}
});
}
}
2016-07-14 13:14:16 +00:00
HTTP_METHODS.reduce((reqProto, method) => {
2016-07-15 19:34:24 +00:00
/**
* Use the given middleware function or object, with optional _uri_ on
* HTTP methods: get, post, put, delete...
* Default _uri_ is "/".
*
* // middleware function will be applied on path "/"
* app.get((req, res, next) => {console.log('Hello')});
*
* // middleware object will be applied on path "/" and
* app.get(new Middleware());
*
* // get a setting value
* app.set('foo', 'bar');
* app.get('foo');
* // => "bar"
*
* @param {String} uri or setting
* @param {Middleware|Function} middleware object or function
2016-07-15 19:34:24 +00:00
* @return {app} for chaining
* @public
*/
2016-07-14 13:14:16 +00:00
const middlewareMethodName = method.toLowerCase();
reqProto[middlewareMethodName] = function(...args) {
2017-01-14 14:02:50 +00:00
let {baseUri, middleware, which} = toParameters(args);
if (middlewareMethodName === 'get' && typeof which === 'string') {
return this.settings.get(which);
}
2017-01-14 14:02:50 +00:00
if (!middleware) {
throw new TypeError(`method takes a middleware ${middlewareMethodName === 'get' ? 'or a string' : ''}`);
}
const router = new Router();
2016-07-14 13:14:16 +00:00
router[middlewareMethodName](baseUri, middleware);
this.routers.push(router);
2016-07-15 19:34:24 +00:00
return this;
};
2016-07-15 19:34:24 +00:00
/**
* Ajax request (get, post, put, delete...).
*
* // HTTP GET method
* httpGet('/route1');
*
* // HTTP GET method
2016-07-15 19:34:24 +00:00
* httpGet({uri: '/route1', data: {'p1': 'val1'});
* // uri invoked => /route1?p1=val1
*
* // HTTP GET method with browser history management
* httpGet({uri: '/api/users', history: {state: {foo: "bar"}, title: 'users page', uri: '/view/users'});
*
* Samples above can be applied on other HTTP methods.
2016-07-15 19:34:24 +00:00
*
* @param {String|Object} uri or object containing uri, http headers, data, history
2016-07-15 19:34:24 +00:00
* @param {Function} success callback
* @param {Function} failure callback
* @public
*/
const httpMethodName = 'http'+method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
reqProto[httpMethodName] = function(request, resolve, reject) {
let {uri, headers, data, history} = request;
if (!uri) {
uri = request;
}
return this._fetch({
uri,
method,
headers,
data,
history
}, resolve, reject);
};
return reqProto;
}, Application.prototype);
2017-01-14 14:02:50 +00:00
export function toParameters(args) {
let baseUri, middleware, router, which;
if (args && args.length > 0) {
if (args.length === 1) {
[which,] = args;
} else {
[baseUri, which,] = args;
}
if (which instanceof Router) {
router = which;
} else if ((which instanceof Middleware) || (typeof which === 'function')) {
middleware = which;
}
}
return {baseUri, middleware, router, which};
}