From 75c968caae787cf3162d013b141583371f947db2 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Fri, 8 Feb 2019 17:50:06 -0800 Subject: [PATCH] Merge pull request #31 from slorber/switch-back-behavior handle new SwitchRouter history back behaviors --- packages/core/src/routers/SwitchRouter.js | 86 +++++++++----- .../routers/__tests__/SwitchRouter-test.js | 106 +++++++++++++++++- 2 files changed, 165 insertions(+), 27 deletions(-) diff --git a/packages/core/src/routers/SwitchRouter.js b/packages/core/src/routers/SwitchRouter.js index 8ceda90e..9f838b7b 100644 --- a/packages/core/src/routers/SwitchRouter.js +++ b/packages/core/src/routers/SwitchRouter.js @@ -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, diff --git a/packages/core/src/routers/__tests__/SwitchRouter-test.js b/packages/core/src/routers/__tests__/SwitchRouter-test.js index 52cac19b..63cf9879 100644 --- a/packages/core/src/routers/__tests__/SwitchRouter-test.js +++ b/packages/core/src/routers/__tests__/SwitchRouter-test.js @@ -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 = () =>
; const StackA = () =>
; const StackB = () =>
; + const StackC = () =>
; 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',