mirror of
https://gitlab.silvrtree.co.uk/martind2000/frontexpress.git
synced 2025-02-11 03:09:15 +00:00
Added browser history management issue #1
This commit is contained in:
parent
9c7681e2b4
commit
390cd54a48
@ -26,7 +26,8 @@ export default class Application {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.routers = [];
|
this.routers = [];
|
||||||
this.DOMLoading = false;
|
this.isDOMLoaded = false;
|
||||||
|
this.isDOMReady = false;
|
||||||
this.settings = new Settings();
|
this.settings = new Settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,33 +77,40 @@ export default class Application {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
listen(callback) {
|
listen(callback) {
|
||||||
document.onreadystatechange = () => {
|
window.onbeforeunload = () => {
|
||||||
const uri = window.location.pathname + window.location.search;
|
this._callMiddlewareExited();
|
||||||
const method = 'GET';
|
};
|
||||||
const request = {method, uri};
|
|
||||||
const response = {status: 200, statusText: 'OK'};
|
|
||||||
|
|
||||||
// gathers all routes impacted by the current browser location
|
window.onpopstate = (event) => {
|
||||||
const currentRoutes = this._routes(uri, method);
|
if (event.state) {
|
||||||
|
const {response, request} = event.state;
|
||||||
|
const currentRoutes = this._routes(request.uri, request.method);
|
||||||
|
|
||||||
// listen dom events
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
this.DOMLoading = true;
|
|
||||||
this._callMiddlewareEntered(currentRoutes, request);
|
this._callMiddlewareEntered(currentRoutes, request);
|
||||||
} else if (document.readyState === 'interactive') {
|
this._callMiddlewareUpdated(currentRoutes, request, response);
|
||||||
if (!this.DOMLoading) {
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onreadystatechange = () => {
|
||||||
|
const request = {method: 'GET', uri: window.location.pathname + window.location.search};
|
||||||
|
const response = {status: 200, statusText: 'OK'};
|
||||||
|
const currentRoutes = this._routes();
|
||||||
|
// DOM state
|
||||||
|
if (document.readyState === 'loading' && !this.isDOMLoaded) {
|
||||||
|
this.isDOMLoaded = true;
|
||||||
|
this._callMiddlewareEntered(currentRoutes, request);
|
||||||
|
} else if (document.readyState === 'interactive' && !this.isDOMReady) {
|
||||||
|
if (!this.isDOMLoaded) {
|
||||||
|
this.isDOMLoaded = true;
|
||||||
this._callMiddlewareEntered(currentRoutes, request);
|
this._callMiddlewareEntered(currentRoutes, request);
|
||||||
}
|
}
|
||||||
|
this.isDOMReady = true;
|
||||||
this._callMiddlewareUpdated(currentRoutes, request, response);
|
this._callMiddlewareUpdated(currentRoutes, request, response);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(request, response);
|
callback(request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
this._callMiddlewareExited();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -183,12 +191,13 @@ export default class Application {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
||||||
_routes(uri, method) {
|
_routes(uri=window.location.pathname + window.location.search, method='GET') {
|
||||||
const currentRoutes = [];
|
const currentRoutes = [];
|
||||||
for (const router of this.routers) {
|
for (const router of this.routers) {
|
||||||
const routes = router.routes(uri, method);
|
const routes = router.routes(uri, method);
|
||||||
currentRoutes.push(...routes);
|
currentRoutes.push(...routes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentRoutes;
|
return currentRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,13 +306,12 @@ export default class Application {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an ajax request.
|
* Make an ajax request. Manage History#pushState if history object set.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
||||||
_fetch({method, uri, headers, data}, resolve, reject) {
|
_fetch({method, uri, headers, data, history}, resolve, reject) {
|
||||||
|
|
||||||
const httpMethodTransformer = this.get(`http ${method} transformer`);
|
const httpMethodTransformer = this.get(`http ${method} transformer`);
|
||||||
if (httpMethodTransformer) {
|
if (httpMethodTransformer) {
|
||||||
uri = httpMethodTransformer.uri ? httpMethodTransformer.uri({uri, headers, data}) : uri;
|
uri = httpMethodTransformer.uri ? httpMethodTransformer.uri({uri, headers, data}) : uri;
|
||||||
@ -323,6 +331,13 @@ export default class Application {
|
|||||||
// invokes http request
|
// invokes http request
|
||||||
this.settings.get('http requester').fetch({method, uri, headers, data},
|
this.settings.get('http requester').fetch({method, uri, headers, data},
|
||||||
(request, response) => {
|
(request, response) => {
|
||||||
|
if (history) {
|
||||||
|
if (history.state) {
|
||||||
|
response.historyState = history.state;
|
||||||
|
}
|
||||||
|
history.state = {request, response};
|
||||||
|
window.history.pushState(history.state, history.title, history.uri);
|
||||||
|
}
|
||||||
this._callMiddlewareUpdated(currentRoutes, request, response);
|
this._callMiddlewareUpdated(currentRoutes, request, response);
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
resolve(request, response);
|
resolve(request, response);
|
||||||
@ -403,21 +418,24 @@ HTTP_METHODS.reduce((reqProto, method) => {
|
|||||||
*
|
*
|
||||||
* // HTTP GET method
|
* // HTTP GET method
|
||||||
* httpGet('/route1');
|
* httpGet('/route1');
|
||||||
|
*
|
||||||
|
* // HTTP GET method
|
||||||
* httpGet({uri: '/route1', data: {'p1': 'val1'});
|
* httpGet({uri: '/route1', data: {'p1': 'val1'});
|
||||||
* // uri invoked => /route1?p1=val1
|
* // uri invoked => /route1?p1=val1
|
||||||
*
|
*
|
||||||
* // HTTP POST method
|
* // HTTP GET method with browser history management
|
||||||
* httpPost('/user');
|
* httpGet({uri: '/api/users', history: {state: {foo: "bar"}, title: 'users page', uri: '/view/users'});
|
||||||
* ...
|
|
||||||
*
|
*
|
||||||
* @param {String|Object} uri or object containing uri, http headers, data
|
* Samples above can be applied on other HTTP methods.
|
||||||
|
*
|
||||||
|
* @param {String|Object} uri or object containing uri, http headers, data, history
|
||||||
* @param {Function} success callback
|
* @param {Function} success callback
|
||||||
* @param {Function} failure callback
|
* @param {Function} failure callback
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
const httpMethodName = 'http'+method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
|
const httpMethodName = 'http'+method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
|
||||||
reqProto[httpMethodName] = function(request, resolve, reject) {
|
reqProto[httpMethodName] = function(request, resolve, reject) {
|
||||||
let {uri, headers, data} = request;
|
let {uri, headers, data, history} = request;
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
uri = request;
|
uri = request;
|
||||||
}
|
}
|
||||||
@ -425,7 +443,8 @@ HTTP_METHODS.reduce((reqProto, method) => {
|
|||||||
uri,
|
uri,
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
data
|
data,
|
||||||
|
history
|
||||||
}, resolve, reject);
|
}, resolve, reject);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/*eslint-env mocha*/
|
/*eslint-env mocha*/
|
||||||
import chai, {assert} from 'chai';
|
import chai, {assert} from 'chai';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
//import jsdom from 'jsdom';
|
||||||
import frontexpress from '../lib/frontexpress';
|
import frontexpress from '../lib/frontexpress';
|
||||||
import Requester from '../lib/requester';
|
import Requester from '../lib/requester';
|
||||||
|
|
||||||
@ -25,18 +26,108 @@ describe('Application', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('listen method', () => {
|
// // JSDOM cannot manage pushState/onpopstate/window.location
|
||||||
let eventFn = {};
|
// see https://github.com/tmpvar/jsdom/issues/1565
|
||||||
|
//
|
||||||
|
// describe('listen method with JSDOM', () => {
|
||||||
|
// before(() => {
|
||||||
|
// // Init DOM with a fake document
|
||||||
|
// // <base> and uri (initial uri) allow to do pushState in jsdom
|
||||||
|
// jsdom.env({
|
||||||
|
// html:`
|
||||||
|
// <html>
|
||||||
|
// <head>
|
||||||
|
// <base href="http://localhost:8080/"></base>
|
||||||
|
// </head>
|
||||||
|
// </html>
|
||||||
|
// `,
|
||||||
|
// url: 'http://localhost:8080/',
|
||||||
|
// done(err, window) {
|
||||||
|
// global.window = window;
|
||||||
|
// global.document = window.document;
|
||||||
|
// window.console = global.console;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let requester;
|
||||||
|
// beforeEach(()=>{
|
||||||
|
// requester = new Requester();
|
||||||
|
// sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
|
||||||
|
// resolve(
|
||||||
|
// {uri, method, headers, data},
|
||||||
|
// {status: 200, statusText: 'OK', responseText:''}
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('history management without state object', (done) => {
|
||||||
|
// const spy_pushState = sinon.spy(window.history, 'pushState');
|
||||||
|
|
||||||
|
// const app = frontexpress();
|
||||||
|
// const m = frontexpress.Middleware();
|
||||||
|
// const spy_middleware = sinon.stub(m, 'updated');
|
||||||
|
|
||||||
|
// app.set('http requester', requester);
|
||||||
|
// app.use('/api/route1', m);
|
||||||
|
// app.listen();
|
||||||
|
|
||||||
|
// const spy_onpopstate = sinon.spy(window, 'onpopstate');
|
||||||
|
|
||||||
|
// app.httpGet({uri:'/api/route1', history: {
|
||||||
|
// uri: '/route1',
|
||||||
|
// title: 'route1'
|
||||||
|
// }},
|
||||||
|
// (req, res) => {
|
||||||
|
// // On request succeeded
|
||||||
|
// assert(spy_onpopstate.callCount === 0);
|
||||||
|
// assert(spy_pushState.calledOnce);
|
||||||
|
// assert(spy_middleware.calledOnce);
|
||||||
|
|
||||||
|
// spy_middleware.reset();
|
||||||
|
// window.history.back();
|
||||||
|
// window.history.forward();
|
||||||
|
// assert(spy_onpopstate.calledOnce);
|
||||||
|
// assert(spy_middleware.calledOnce);
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
describe('listen method', () => {
|
||||||
|
// Here I cannot use jsdon to make these tests :(
|
||||||
|
// JSDOM cannot simulate readyState changes
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const browserHistory = [{uri: '/'}];
|
||||||
|
let browserHistoryIndex = 0;
|
||||||
|
|
||||||
global.document = {};
|
global.document = {};
|
||||||
global.window = {
|
global.window = {
|
||||||
addEventListener(eventType, callback) {
|
|
||||||
eventFn[eventType] = callback;
|
|
||||||
},
|
|
||||||
location: {
|
location: {
|
||||||
pathname: '/route1',
|
pathname: '/route1',
|
||||||
search: '?a=b'
|
search: '?a=b'
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
pushState(state, title, pathname) {
|
||||||
|
browserHistory.push({uri: pathname, state});
|
||||||
|
browserHistoryIndex++;
|
||||||
|
global.window.location.pathname = browserHistory[browserHistoryIndex].uri;
|
||||||
|
},
|
||||||
|
forward() {
|
||||||
|
browserHistoryIndex++;
|
||||||
|
global.window.location.pathname = browserHistory[browserHistoryIndex].uri;
|
||||||
|
if (browserHistory[browserHistoryIndex].state) {
|
||||||
|
window.onpopstate({state:browserHistory[browserHistoryIndex].state});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
back() {
|
||||||
|
browserHistoryIndex--;
|
||||||
|
global.window.location.pathname = browserHistory[browserHistoryIndex].uri;
|
||||||
|
if (browserHistory[browserHistoryIndex].state) {
|
||||||
|
window.onpopstate({state: browserHistory[browserHistoryIndex].state});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -93,13 +184,102 @@ describe('Application', () => {
|
|||||||
app.use('/route1', m);
|
app.use('/route1', m);
|
||||||
app.listen(() => {
|
app.listen(() => {
|
||||||
//simulate beforeunload
|
//simulate beforeunload
|
||||||
eventFn['beforeunload']();
|
window.onbeforeunload();
|
||||||
});
|
});
|
||||||
|
|
||||||
//simulate readystatechange
|
//simulate readystatechange
|
||||||
document.readyState = 'interactive';
|
document.readyState = 'interactive';
|
||||||
document.onreadystatechange();
|
document.onreadystatechange();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('history management without state object', (done) => {
|
||||||
|
const requester = new Requester();
|
||||||
|
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
|
||||||
|
resolve(
|
||||||
|
{uri, method, headers, data},
|
||||||
|
{status: 200, statusText: 'OK', responseText:''}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const spy_pushState = sinon.spy(window.history, 'pushState');
|
||||||
|
|
||||||
|
const app = frontexpress();
|
||||||
|
const m = frontexpress.Middleware();
|
||||||
|
const spy_middleware = sinon.stub(m, 'updated');
|
||||||
|
|
||||||
|
app.set('http requester', requester);
|
||||||
|
app.use('/api/route1', m);
|
||||||
|
app.listen();
|
||||||
|
|
||||||
|
const spy_onpopstate = sinon.spy(window, 'onpopstate');
|
||||||
|
|
||||||
|
app.httpGet({uri:'/api/route1', history: {
|
||||||
|
uri: '/route1',
|
||||||
|
title: 'route1'
|
||||||
|
}},
|
||||||
|
(req, res) => {
|
||||||
|
// On request succeeded
|
||||||
|
assert(spy_onpopstate.callCount === 0);
|
||||||
|
assert(spy_pushState.calledOnce);
|
||||||
|
assert(spy_middleware.calledOnce);
|
||||||
|
|
||||||
|
spy_middleware.reset();
|
||||||
|
window.history.back();
|
||||||
|
window.history.forward();
|
||||||
|
assert(spy_onpopstate.calledOnce);
|
||||||
|
assert(spy_middleware.calledOnce);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('history management with state object', (done) => {
|
||||||
|
let stateObj;
|
||||||
|
const requester = new Requester();
|
||||||
|
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
|
||||||
|
resolve(
|
||||||
|
{uri, method, headers, data},
|
||||||
|
{status: 200, statusText: 'OK', responseText:''}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const spy_pushState = sinon.spy(window.history, 'pushState');
|
||||||
|
|
||||||
|
const app = frontexpress();
|
||||||
|
const m = frontexpress.Middleware();
|
||||||
|
const spy_middleware = sinon.stub(m, 'updated', (req, res) => {
|
||||||
|
stateObj = res.historyState;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.set('http requester', requester);
|
||||||
|
app.use('/api/route1', m);
|
||||||
|
app.listen();
|
||||||
|
|
||||||
|
const spy_onpopstate = sinon.spy(window, 'onpopstate');
|
||||||
|
|
||||||
|
app.httpGet({uri:'/api/route1', history: {
|
||||||
|
uri: '/route1',
|
||||||
|
title: 'route1',
|
||||||
|
state: {a: 'b', c: 'd'}
|
||||||
|
}},
|
||||||
|
(req, res) => {
|
||||||
|
// On request succeeded
|
||||||
|
assert(spy_onpopstate.callCount === 0);
|
||||||
|
assert(spy_pushState.calledOnce);
|
||||||
|
assert(spy_middleware.calledOnce);
|
||||||
|
|
||||||
|
spy_middleware.reset();
|
||||||
|
window.history.back();
|
||||||
|
window.history.forward();
|
||||||
|
assert(spy_onpopstate.calledOnce);
|
||||||
|
assert(spy_middleware.calledOnce);
|
||||||
|
assert(stateObj);
|
||||||
|
assert(stateObj.a === 'b');
|
||||||
|
assert(stateObj.c === 'd');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('set/get setting method', () => {
|
describe('set/get setting method', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user