Skip to content

Commit

Permalink
Merge pull request #3 from subuta/feature/add-popstate
Browse files Browse the repository at this point in the history
Feature/add popstate
  • Loading branch information
subuta committed Nov 7, 2016
2 parents 91f7e20 + d9c6e1a commit cd62e3c
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 338 deletions.
1 change: 1 addition & 0 deletions lib/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const BACK = '@@router-redux/HISTORY_BACK';
export const FORWARD = '@@router-redux/HISTORY_FORWARD';

export const isHistoryAction = (str) => !!str && str.indexOf("@@router-redux/HISTORY") === 0;
export const isRouteAction = (str) => !!str && str.indexOf("@@router-redux/ROUTE") === 0;

export const transformLocationToPath = (location) => {
const fullPath = location.href.replace(location.origin, '');
Expand Down
37 changes: 31 additions & 6 deletions lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
transformLocationToPath,
initialRouteResolved,
isHistoryAction,
isRouteAction,
setNextRoute,
routeError,
getQuery
} from './actions.js';

import {
getRoutes,
routeErrorHandlerCreator,
createRoute,
findRouteKeyByPath
} from './router.js';
Expand All @@ -36,13 +37,36 @@ const callHistoryAndUpdateAction = (history, action) => {
} else if (action.type === FORWARD) {
history.forward();
}

// if history action then override payload as location
return {
...action,
payload: createRoute(transformLocationToPath(location), getQuery(location)) // pass latest `path`
};
};

// check and handle routeError
export const routeErrorHandlerCreator = (store) => (data) => {
const {dispatch, getState} = store;
const routes = getRoutes();

// if called with falsy data(includes Error)
if (!data || data instanceof Error) {
dispatch(routeError(data));

if (routes.Error) {
// call error handler.
routes.Error({dispatch, state: getState()});
} else {
// put some messages with no onError handler.
console.warn('You should register router.onError to handle this routing error. data =', data);
}
return true;
}

return false;
};

export default (store) => (next) => (action) => {
// use current state
const {dispatch, getState} = store;
Expand All @@ -52,7 +76,7 @@ export default (store) => (next) => (action) => {
const history = window.history;

// pass-through other action.
if (!isHistoryAction(action.type)) {
if (!isHistoryAction(action.type) && !isRouteAction(action.type)) {
next(action);
return;
}
Expand All @@ -64,7 +88,8 @@ export default (store) => (next) => (action) => {
}

const routes = getRoutes();
const nextPath = action.payload;
const _next = action.payload;
const nextPath = _next && _next.path ? _next.path : _next;
const nextRoute = findRouteKeyByPath(nextPath);

// call onLeave hook
Expand All @@ -74,14 +99,14 @@ export default (store) => (next) => (action) => {

// call onEnter hook
if (routes[nextRoute] && routes[nextRoute].onEnter) {
// call initialRouteResolved action on initial onEnter call.
const isInitalRouteResolved = getIsInitalRouteResolved(getState());
!isInitalRouteResolved && next(initialRouteResolved());
next(setNextRoute(createRoute(nextPath, nextPath.split('?')[1])));

// if onEnter accepts callback.
if (routes[nextRoute].onEnter.length == 2) {
routes[nextRoute].onEnter({dispatch, state: getState()}, (data = true) => {
// call initialRouteResolved action on initial onEnter call.
const isInitalRouteResolved = getIsInitalRouteResolved(getState());
!isInitalRouteResolved && next(initialRouteResolved());
// return if routeError exists.
if (routeErrorHandler(data)) return;
// delay action dispatch until onEnter's callback called.
Expand Down
10 changes: 8 additions & 2 deletions lib/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import {
INITIAL_ROUTE_RESOLVED,
ROUTE_ERROR,
SET_NEXT_ROUTE,
isHistoryAction
isHistoryAction,
transformLocationToPath,
getQuery
} from "./actions.js";

import {
createRoute
} from './router.js';

// defaults to raw location.
const initialState = {
current: null,
current: createRoute(transformLocationToPath(location), getQuery(location)),
last: null,
next: null,
isInitialRouteResolved: false,
Expand Down
60 changes: 11 additions & 49 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,56 +38,12 @@ export const createRoute = (path, query = '') => {
}
};

// check and handle routeError
export const routeErrorHandlerCreator = (store) => (data) => {
const {dispatch, getState} = store;
const routes = getRoutes();

// if called with falsy data(includes Error)
if (!data || data instanceof Error) {
dispatch(routeError(data));

if (routes.Error) {
// call error handler.
routes.Error({dispatch, state: getState()});
} else {
// put some messages with no onError handler.
console.warn('You should register router.onError to handle this routing error. data =', data);
}
return true;
}

return false;
};

const routerCreator = (store) => {
const {dispatch, getState} = store;
const routeErrorHandler = routeErrorHandlerCreator(store);

// initialize routes.
routes = {};

// delay until onEnter registered.
requestAnimationFrame(() => {
// set initial route.
dispatch(routeChange(createRoute(transformLocationToPath(location), getQuery(location))));
// call current route's onEnter.
const currentPath = getCurrent(getState()) && getCurrent(getState()).path;
const currentRoute = findRouteKeyByPath(currentPath);

if (routes[currentRoute] && routes[currentRoute].onEnter) {
// if handler accepts callback.
if (routes[currentRoute].onEnter.length == 2) {
routes[currentRoute].onEnter({dispatch, state: getState()}, (data = true) => {
dispatch(initialRouteResolved());
routeErrorHandler(data);
});
} else {
routes[currentRoute].onEnter({dispatch, state: getState()});
}
}
});

// call on Route enter.
const onError = (handler) => {
routes.Error = handler;
Expand All @@ -103,22 +59,28 @@ const routerCreator = (store) => {
};

// call on Route leave.
const onLeave = (pathname, handler) => {
routes[pathname] = routes[pathname] || {};
routes[pathname] = {
...routes[pathname],
const onLeave = (path, handler) => {
routes[path] = routes[path] || {};
routes[path] = {
...routes[path],
onLeave: handler
};
};

const onChange = (e) => {
const onChange = () => {
dispatch(routeChange(createRoute(transformLocationToPath(location), getQuery(location))));
};

// bind onChange to popstate.
window.addEventListener('popstate', onChange);
const destroy = () => window.removeEventListener('popstate', onChange);

// delay until onEnter registered.
requestAnimationFrame(() => {
// set initial route.
onChange();
});

return {
onEnter,
onError,
Expand Down
17 changes: 16 additions & 1 deletion spec/actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
go,
back,
forward,
isHistoryAction
isHistoryAction,
isRouteAction
} from 'lib/actions.js';

describe('actions', function() {
Expand Down Expand Up @@ -93,6 +94,20 @@ describe('isHistoryAction', function() {
});
});

describe('isRouteAction', function() {
it('should return true with valid string', function(){
assert.equal(isRouteAction('@@router-redux/ROUTE_CHANGE'), true);
});

it('should return true with invalid string', function(){
assert.equal(isRouteAction('@@router-redux/HISTORY_PUSH_STATE'), false);
});

it('should return false with null', function(){
assert.equal(isRouteAction(null), false);
});
});

describe('transformLocationToPath', function() {
beforeEach(function(){
history.pushState(null, null, '/');
Expand Down

0 comments on commit cd62e3c

Please sign in to comment.