diff --git a/packages/react-navigation/src/routers/StackRouter.js b/packages/react-navigation/src/routers/StackRouter.js index 001047c3..7d09bd26 100644 --- a/packages/react-navigation/src/routers/StackRouter.js +++ b/packages/react-navigation/src/routers/StackRouter.js @@ -46,24 +46,42 @@ export default (routeConfigs, stackConfig = {}) => { const initialRouteName = stackConfig.initialRouteName || routeNames[0]; const initialChildRouter = childRouters[initialRouteName]; - const paths = stackConfig.paths || {}; + const pathsByRouteNames = { ...stackConfig.paths } || {}; + let paths = []; // Build paths for each route routeNames.forEach(routeName => { - let pathPattern = paths[routeName] || routeConfigs[routeName].path; - const matchExact = !!pathPattern && !childRouters[routeName]; - if (typeof pathPattern !== 'string') { + let pathPattern = + pathsByRouteNames[routeName] || routeConfigs[routeName].path; + let matchExact = !!pathPattern && !childRouters[routeName]; + if (pathPattern === undefined) { pathPattern = routeName; } const keys = []; - let re = pathToRegexp(pathPattern, keys); + let re, toPath, priority; + if (typeof pathPattern === 'string') { + // pathPattern may be either a string or a regexp object according to path-to-regexp docs. + re = pathToRegexp(pathPattern, keys); + toPath = pathToRegexp.compile(pathPattern); + priority = 0; + } else { + // for wildcard match + re = pathToRegexp('*', keys); + toPath = () => ''; + matchExact = true; + priority = -1; + } if (!matchExact) { const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys); re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`); } - paths[routeName] = { re, keys, toPath: pathToRegexp.compile(pathPattern) }; + pathsByRouteNames[routeName] = { re, keys, toPath, priority }; }); + paths = Object.entries(pathsByRouteNames); + /* $FlowFixMe */ + paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority); + return { getComponentForState(state) { const activeChildRoute = state.routes[state.index]; @@ -293,7 +311,7 @@ export default (routeConfigs, stackConfig = {}) => { const route = state.routes[state.index]; const routeName = route.routeName; const screen = getScreenForRouteName(routeConfigs, routeName); - const subPath = paths[routeName].toPath(route.params); + const subPath = pathsByRouteNames[routeName].toPath(route.params); let path = subPath; let params = route.params; if (screen && screen.router) { @@ -328,7 +346,7 @@ export default (routeConfigs, stackConfig = {}) => { let pathMatchKeys; // eslint-disable-next-line no-restricted-syntax - for (const [routeName, path] of Object.entries(paths)) { + for (const [routeName, path] of paths) { const { re, keys } = path; pathMatch = re.exec(pathNameToResolve); if (pathMatch && pathMatch.length) { @@ -340,6 +358,13 @@ export default (routeConfigs, stackConfig = {}) => { // We didn't match -- return null if (!matchedRouteName) { + // If the path is empty (null or empty string) + // just return the initial route action + if (!pathToResolve) { + return NavigationActions.navigate({ + routeName: initialRouteName, + }); + } return null; } @@ -353,6 +378,9 @@ export default (routeConfigs, stackConfig = {}) => { nestedAction = childRouters[matchedRouteName].getActionForPathAndParams( pathMatch.slice(pathMatchKeys.length).join('/') + nestedQueryString ); + if (!nestedAction) { + return null; + } } // reduce the items of the query string. any query params may diff --git a/packages/react-navigation/src/routers/__tests__/StackRouter-test.js b/packages/react-navigation/src/routers/__tests__/StackRouter-test.js index c995e774..6b4e2a4d 100644 --- a/packages/react-navigation/src/routers/__tests__/StackRouter-test.js +++ b/packages/react-navigation/src/routers/__tests__/StackRouter-test.js @@ -54,6 +54,10 @@ const TestStackRouter = StackRouter({ main: { screen: MainNavigator, }, + baz: { + path: null, + screen: FooNavigator, + }, auth: { screen: AuthNavigator, }, @@ -277,6 +281,22 @@ describe('StackRouter', () => { }); }); + test('Correctly parses a path to the router connected to another router through a pure wildcard route into an action chain', () => { + const uri = 'b/123'; + const action = TestStackRouter.getActionForPathAndParams(uri); + expect(action).toEqual({ + type: NavigationActions.NAVIGATE, + routeName: 'baz', + action: { + type: NavigationActions.NAVIGATE, + routeName: 'bar', + params: { + barThing: '123', + }, + }, + }); + }); + test('Correctly returns null action for non-existent path', () => { const uri = 'asdf/1234'; const action = TestStackRouter.getActionForPathAndParams(uri); @@ -944,6 +964,89 @@ describe('StackRouter', () => { expect(params.bazId).toEqual('321'); }); + test('Gets deep path with pure wildcard match', () => { + const ScreenA = () =>
; + const ScreenB = () =>
; + const ScreenC = () =>
; + ScreenA.router = StackRouter({ + Boo: { path: 'boo', screen: ScreenC }, + Baz: { path: 'baz/:bazId', screen: ScreenB }, + }); + ScreenC.router = StackRouter({ + Boo2: { path: '', screen: ScreenB }, + }); + const router = StackRouter({ + Foo: { + path: null, + screen: ScreenA, + }, + Bar: { + screen: ScreenB, + }, + }); + + { + const state = { + index: 0, + routes: [ + { + index: 1, + key: 'Foo', + routeName: 'Foo', + params: { + id: '123', + }, + routes: [ + { + index: 0, + key: 'Boo', + routeName: 'Boo', + routes: [{ key: 'Boo2', routeName: 'Boo2' }], + }, + { key: 'Baz', routeName: 'Baz', params: { bazId: '321' } }, + ], + }, + { key: 'Bar', routeName: 'Bar' }, + ], + }; + const { path, params } = router.getPathAndParamsForState(state); + expect(path).toEqual('baz/321'); + /* $FlowFixMe: params.id has to exist */ + expect(params.id).toEqual('123'); + /* $FlowFixMe: params.bazId has to exist */ + expect(params.bazId).toEqual('321'); + } + + { + const state = { + index: 0, + routes: [ + { + index: 0, + key: 'Foo', + routeName: 'Foo', + params: { + id: '123', + }, + routes: [ + { + index: 0, + key: 'Boo', + routeName: 'Boo', + routes: [{ key: 'Boo2', routeName: 'Boo2' }], + }, + { key: 'Baz', routeName: 'Baz', params: { bazId: '321' } }, + ], + }, + { key: 'Bar', routeName: 'Bar' }, + ], + }; + const { path, params } = router.getPathAndParamsForState(state); + expect(path).toEqual('boo/'); + expect(params).toEqual({ id: '123' }); + } + }); + test('Maps old actions (uses "Handles the reset action" test)', () => { global.console.warn = jest.fn(); const router = StackRouter({