Merge pull request #31 from slorber/switch-back-behavior

handle new SwitchRouter history back behaviors
This commit is contained in:
Brent Vatne
2019-02-08 17:50:06 -08:00
parent f66f23e7e7
commit 75c968caae
2 changed files with 165 additions and 27 deletions

View File

@@ -29,11 +29,18 @@ export default (routeConfigs, config = {}) => {
const initialRouteParams = config.initialRouteParams;
const initialRouteName = config.initialRouteName || order[0];
const backBehavior = config.backBehavior || 'none';
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
const resetOnBlur = config.hasOwnProperty('resetOnBlur')
? config.resetOnBlur
: true;
const initialRouteIndex = order.indexOf(initialRouteName);
if (initialRouteIndex === -1) {
throw new Error(
`Invalid initialRouteName '${initialRouteName}'.` +
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
);
}
const childRouters = {};
order.forEach(routeName => {
childRouters[routeName] = null;
@@ -57,13 +64,6 @@ export default (routeConfigs, config = {}) => {
getActionForPathAndParams,
} = createPathParser(childRouters, routeConfigs, config);
if (initialRouteIndex === -1) {
throw new Error(
`Invalid initialRouteName '${initialRouteName}'.` +
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
);
}
function resetChildRoute(routeName) {
let initialParams =
routeName === initialRouteName ? initialRouteParams : undefined;
@@ -88,35 +88,56 @@ export default (routeConfigs, config = {}) => {
};
}
function getNextState(prevState, possibleNextState) {
if (!prevState) {
return possibleNextState;
function getNextState(action, prevState, possibleNextState) {
function updateNextStateHistory(nextState) {
if (backBehavior !== 'history') {
return nextState;
}
let nextRouteKeyHistory = prevState.routeKeyHistory;
if (action.type === NavigationActions.NAVIGATE) {
nextRouteKeyHistory = [...prevState.routeKeyHistory]; // copy
const keyToAdd = nextState.routes[nextState.index].key;
nextRouteKeyHistory = nextRouteKeyHistory.filter(k => k !== keyToAdd); // dedup
nextRouteKeyHistory.push(keyToAdd);
} else if (action.type === NavigationActions.BACK) {
nextRouteKeyHistory = [...prevState.routeKeyHistory]; // copy
nextRouteKeyHistory.pop();
}
return {
...nextState,
routeKeyHistory: nextRouteKeyHistory,
};
}
let nextState;
if (prevState.index !== possibleNextState.index && resetOnBlur) {
let nextState = possibleNextState;
if (
prevState &&
prevState.index !== possibleNextState.index &&
resetOnBlur
) {
const prevRouteName = prevState.routes[prevState.index].routeName;
const nextRoutes = [...possibleNextState.routes];
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
return {
nextState = {
...possibleNextState,
routes: nextRoutes,
};
} else {
nextState = possibleNextState;
}
return nextState;
return updateNextStateHistory(nextState);
}
function getInitialState() {
const routes = order.map(resetChildRoute);
return {
const initialState = {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
if (backBehavior === 'history') {
const initialKey = routes[initialRouteIndex].key;
initialState['routeKeyHistory'] = [initialKey];
}
return initialState;
}
return {
@@ -165,7 +186,7 @@ export default (routeConfigs, config = {}) => {
if (activeChildState && activeChildState !== activeChildLastState) {
const routes = [...state.routes];
routes[state.index] = activeChildState;
return getNextState(prevState, {
return getNextState(action, prevState, {
...state,
routes,
});
@@ -177,8 +198,21 @@ export default (routeConfigs, config = {}) => {
const isBackEligible =
action.key == null || action.key === activeChildLastState.key;
if (action.type === NavigationActions.BACK) {
if (isBackEligible && shouldBackNavigateToInitialRoute) {
if (isBackEligible && backBehavior === 'initialRoute') {
activeChildIndex = initialRouteIndex;
} else if (isBackEligible && backBehavior === 'order') {
activeChildIndex = Math.max(0, activeChildIndex - 1);
}
// The history contains current route, so we can only go back
// if there is more than one item in the history
else if (
isBackEligible &&
backBehavior === 'history' &&
state.routeKeyHistory.length > 1
) {
const routeKey =
state.routeKeyHistory[state.routeKeyHistory.length - 2];
activeChildIndex = order.indexOf(routeKey);
} else {
return state;
}
@@ -226,7 +260,7 @@ export default (routeConfigs, config = {}) => {
routes,
index: activeChildIndex,
};
return getNextState(prevState, nextState);
return getNextState(action, prevState, nextState);
} else if (
newChildState === childState &&
state.index === activeChildIndex &&
@@ -250,7 +284,7 @@ export default (routeConfigs, config = {}) => {
...lastRoute,
params,
};
return getNextState(prevState, {
return getNextState(action, prevState, {
...state,
routes,
});
@@ -258,7 +292,7 @@ export default (routeConfigs, config = {}) => {
}
if (activeChildIndex !== state.index) {
return getNextState(prevState, {
return getNextState(action, prevState, {
...state,
index: activeChildIndex,
});
@@ -302,7 +336,7 @@ export default (routeConfigs, config = {}) => {
}
if (index !== state.index || routes !== state.routes) {
return getNextState(prevState, {
return getNextState(action, prevState, {
...state,
index,
routes,

View File

@@ -61,9 +61,12 @@ describe('SwitchRouter', () => {
expect(state3.index).toEqual(1);
});
test('handles back if given a backBehavior', () => {
test('handles initialRoute backBehavior', () => {
const router = getExampleRouter({ backBehavior: 'initialRoute' });
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routeKeyHistory).toBeUndefined();
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state
@@ -78,6 +81,75 @@ describe('SwitchRouter', () => {
expect(state3.index).toEqual(0);
});
test('handles order backBehavior', () => {
const routerHelper = new ExampleRouterHelper({ backBehavior: 'order' });
expect(routerHelper.getCurrentState().routeKeyHistory).toBeUndefined();
expect(
routerHelper.applyAction({
type: NavigationActions.NAVIGATE,
routeName: 'C',
})
).toMatchObject({ index: 2 });
expect(
routerHelper.applyAction({ type: NavigationActions.BACK })
).toMatchObject({ index: 1 });
expect(
routerHelper.applyAction({ type: NavigationActions.BACK })
).toMatchObject({ index: 0 });
expect(
routerHelper.applyAction({ type: NavigationActions.BACK })
).toMatchObject({ index: 0 });
});
test('handles history backBehavior', () => {
const routerHelper = new ExampleRouterHelper({ backBehavior: 'history' });
expect(routerHelper.getCurrentState().routeKeyHistory).toMatchObject(['A']);
expect(
routerHelper.applyAction({
type: NavigationActions.NAVIGATE,
routeName: 'B',
})
).toMatchObject({ index: 1, routeKeyHistory: ['A', 'B'] });
expect(
routerHelper.applyAction({
type: NavigationActions.NAVIGATE,
routeName: 'A',
})
).toMatchObject({ index: 0, routeKeyHistory: ['B', 'A'] });
expect(
routerHelper.applyAction({
type: NavigationActions.NAVIGATE,
routeName: 'C',
})
).toMatchObject({ index: 2, routeKeyHistory: ['B', 'A', 'C'] });
expect(
routerHelper.applyAction({
type: NavigationActions.NAVIGATE,
routeName: 'A',
})
).toMatchObject({ index: 0, routeKeyHistory: ['B', 'C', 'A'] });
expect(
routerHelper.applyAction({ type: NavigationActions.BACK })
).toMatchObject({ index: 2, routeKeyHistory: ['B', 'C'] });
expect(
routerHelper.applyAction({ type: NavigationActions.BACK })
).toMatchObject({ index: 1, routeKeyHistory: ['B'] });
expect(
routerHelper.applyAction({ type: NavigationActions.BACK })
).toMatchObject({ index: 1, routeKeyHistory: ['B'] });
});
test('handles nested actions', () => {
const router = getExampleRouter();
const state = router.getStateForAction({ type: NavigationActions.INIT });
@@ -200,10 +272,33 @@ describe('SwitchRouter', () => {
});
});
// A simple helper that makes it easier to write basic routing tests
// As we generally want to apply one action after the other,
// it's often convenient to manipulate a structure that keeps the router state
class ExampleRouterHelper {
constructor(config) {
this._router = getExampleRouter(config);
this._currentState = this._router.getStateForAction({
type: NavigationActions.INIT,
});
}
applyAction = action => {
this._currentState = this._router.getStateForAction(
action,
this._currentState
);
return this._currentState;
};
getCurrentState = () => this._currentState;
}
const getExampleRouter = (config = {}) => {
const PlainScreen = () => <div />;
const StackA = () => <div />;
const StackB = () => <div />;
const StackC = () => <div />;
StackA.router = StackRouter({
A1: PlainScreen,
@@ -215,6 +310,11 @@ const getExampleRouter = (config = {}) => {
B2: PlainScreen,
});
StackC.router = StackRouter({
C1: PlainScreen,
C2: PlainScreen,
});
const router = SwitchRouter(
{
A: {
@@ -225,6 +325,10 @@ const getExampleRouter = (config = {}) => {
screen: StackB,
path: 'great/path',
},
C: {
screen: StackC,
path: 'pathC',
},
},
{
initialRouteName: 'A',