Files
react-navigation/src/routers/StackRouter.js
2017-06-06 17:24:13 -05:00

433 lines
14 KiB
JavaScript

/* @flow */
import pathToRegexp from 'path-to-regexp';
import NavigationActions from '../NavigationActions';
import createConfigGetter from './createConfigGetter';
import getScreenForRouteName from './getScreenForRouteName';
import StateUtils from '../StateUtils';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
import type {
NavigationAction,
NavigationComponent,
NavigationNavigateAction,
NavigationRouter,
NavigationRouteConfigMap,
NavigationResetAction,
NavigationParams,
NavigationState,
NavigationStackAction,
NavigationStackRouterConfig,
} from '../TypeDefinition';
const uniqueBaseId = `id-${Date.now()}`;
let uuidCount = 0;
function _getUuid() {
return `${uniqueBaseId}-${uuidCount++}`;
}
export default (
routeConfigs: NavigationRouteConfigMap,
stackConfig: NavigationStackRouterConfig = {}
): NavigationRouter<*, *, *> => {
// Fail fast on invalid route definitions
validateRouteConfigMap(routeConfigs);
const childRouters = {};
const routeNames = Object.keys(routeConfigs);
routeNames.forEach((routeName: string) => {
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen && screen.router) {
// If it has a router it's a navigator.
childRouters[routeName] = screen.router;
} else {
// If it doesn't have router it's an ordinary React component.
childRouters[routeName] = null;
}
});
const { initialRouteParams } = stackConfig;
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
const initialChildRouter = childRouters[initialRouteName];
const paths = stackConfig.paths || {};
routeNames.forEach((routeName: string) => {
let pathPattern = paths[routeName] || routeConfigs[routeName].path;
const matchExact = !!pathPattern && !childRouters[routeName];
if (typeof pathPattern !== 'string') {
pathPattern = routeName;
}
const keys = [];
let re = pathToRegexp(pathPattern, keys);
if (!matchExact) {
const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys);
re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`);
}
/* $FlowFixMe */
paths[routeName] = { re, keys, toPath: pathToRegexp.compile(pathPattern) };
});
let inProgressNavigationRouteName = null;
return {
getComponentForState(state: NavigationState): NavigationComponent {
const activeChildRoute = state.routes[state.index];
const { routeName } = activeChildRoute;
if (childRouters[routeName]) {
return childRouters[routeName].getComponentForState(activeChildRoute);
}
return getScreenForRouteName(routeConfigs, routeName);
},
getComponentForRouteName(routeName: string): NavigationComponent {
return getScreenForRouteName(routeConfigs, routeName);
},
getStateForAction(
passedAction: NavigationStackAction,
state: ?NavigationState
) {
const action = NavigationActions.mapDeprecatedActionAndWarn(passedAction);
// Set up the initial state if needed
if (!state) {
let route = {};
if (
action.type === NavigationActions.NAVIGATE &&
childRouters[action.routeName] !== undefined
) {
return {
index: 0,
routes: [
{
...action,
type: undefined,
key: `Init-${_getUuid()}`,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params ||
action.params ||
initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
route = {
...route,
routeName: initialRouteName,
key: `Init-${_getUuid()}`,
...(params ? { params } : {}),
};
// eslint-disable-next-line no-param-reassign
state = {
index: 0,
routes: [route],
};
}
if (passedAction.type === 'Navigation/BACK') {
inProgressNavigationRouteName = null;
}
if (
action.type === NavigationActions.NAVIGATE &&
passedAction.routeName !== 'DrawerOpen' &&
passedAction.routeName !== 'DrawerClose'
) {
// Check if action wants to route to the route that is in-progress navigating
if (
inProgressNavigationRouteName !== null &&
inProgressNavigationRouteName === passedAction.routeName
) {
inProgressNavigationRouteName = null;
return;
}
inProgressNavigationRouteName = passedAction.routeName;
}
// Check if a child scene wants to handle the action as long as it is not a reset to the root stack
if (action.type !== NavigationActions.RESET || action.key !== null) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
: -1;
const childIndex = keyIndex >= 0 ? keyIndex : state.index;
const childRoute = state.routes[childIndex];
const childRouter = childRouters[childRoute.routeName];
if (childRouter) {
const route = childRouter.getStateForAction(action, childRoute);
if (route === null) {
return state;
}
if (route && route !== childRoute) {
return StateUtils.replaceAt(state, childRoute.key, route);
}
}
}
// Handle explicit push navigation action
if (
action.type === NavigationActions.NAVIGATE &&
childRouters[action.routeName] !== undefined
) {
const childRouter = childRouters[action.routeName];
let route;
if (childRouter) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
...childRouter.getStateForAction(childAction),
key: _getUuid(),
routeName: action.routeName,
};
} else {
route = {
params: action.params,
key: _getUuid(),
routeName: action.routeName,
};
}
return StateUtils.push(state, route);
}
// Handle navigation to other child routers that are not yet pushed
if (action.type === NavigationActions.NAVIGATE) {
const childRouterNames = Object.keys(childRouters);
for (let i = 0; i < childRouterNames.length; i++) {
const childRouterName = childRouterNames[i];
const childRouter = childRouters[childRouterName];
if (childRouter) {
// For each child router, start with a blank state
const initChildRoute = childRouter.getStateForAction(
NavigationActions.init()
);
// Then check to see if the router handles our navigate action
const navigatedChildRoute = childRouter.getStateForAction(
action,
initChildRoute
);
let routeToPush = null;
if (navigatedChildRoute === null) {
// Push the route if the router has 'handled' the action and returned null
routeToPush = initChildRoute;
} else if (navigatedChildRoute !== initChildRoute) {
// Push the route if the state has changed in response to this navigation
routeToPush = navigatedChildRoute;
}
if (routeToPush) {
return StateUtils.push(state, {
...routeToPush,
key: _getUuid(),
routeName: childRouterName,
});
}
}
}
}
if (action.type === NavigationActions.SET_PARAMS) {
inProgressNavigationRouteName = null;
const lastRoute = state.routes.find(
/* $FlowFixMe */
(route: *) => route.key === action.key
);
if (lastRoute) {
const params = {
...lastRoute.params,
...action.params,
};
const routes = [...state.routes];
routes[state.routes.indexOf(lastRoute)] = {
...lastRoute,
params,
};
return {
...state,
routes,
};
}
}
if (action.type === NavigationActions.RESET) {
const resetAction: NavigationResetAction = action;
return {
...state,
routes: resetAction.actions.map(
(childAction: NavigationNavigateAction) => {
const router = childRouters[childAction.routeName];
if (router) {
return {
...childAction,
...router.getStateForAction(childAction),
routeName: childAction.routeName,
key: _getUuid(),
};
}
const route = {
...childAction,
key: _getUuid(),
};
delete route.type;
return route;
}
),
index: action.index,
};
}
if (action.type === NavigationActions.BACK) {
let backRouteIndex = null;
inProgressNavigationRouteName = null;
if (action.key) {
const backRoute = state.routes.find(
/* $FlowFixMe */
(route: *) => route.key === action.key
);
/* $FlowFixMe */
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex == null) {
return StateUtils.pop(state);
}
if (backRouteIndex > 0) {
return {
...state,
routes: state.routes.slice(0, backRouteIndex),
index: backRouteIndex - 1,
};
}
}
return state;
},
getPathAndParamsForState(
state: NavigationState
): { path: string, params?: NavigationParams } {
const route = state.routes[state.index];
const routeName = route.routeName;
const screen = getScreenForRouteName(routeConfigs, routeName);
/* $FlowFixMe */
const subPath = paths[routeName].toPath(route.params);
let path = subPath;
let params = route.params;
if (screen && screen.router) {
// If it has a router it's a navigator.
// If it doesn't have router it's an ordinary React component.
const child = screen.router.getPathAndParamsForState(route);
path = subPath ? `${subPath}/${child.path}` : child.path;
params = child.params ? { ...params, ...child.params } : params;
}
return {
path,
params,
};
},
getActionForPathAndParams(pathToResolve: string): ?NavigationAction {
// If the path is empty (null or empty string)
// just return the initial route action
if (!pathToResolve) {
return NavigationActions.navigate({
routeName: initialRouteName,
});
}
const [pathNameToResolve, queryString] = pathToResolve.split('?');
// Attempt to match `pathNameToResolve` with a route in this router's
// routeConfigs
let matchedRouteName;
let pathMatch;
let pathMatchKeys;
// eslint-disable-next-line no-restricted-syntax
for (const [routeName, path] of Object.entries(paths)) {
/* $FlowFixMe */
const { re, keys } = path;
pathMatch = re.exec(pathNameToResolve);
if (pathMatch && pathMatch.length) {
pathMatchKeys = keys;
matchedRouteName = routeName;
break;
}
}
// We didn't match -- return null
if (!matchedRouteName) {
return null;
}
// Determine nested actions:
// If our matched route for this router is a child router,
// get the action for the path AFTER the matched path for this
// router
let nestedAction;
if (childRouters[matchedRouteName]) {
nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
/* $FlowFixMe */
pathMatch.slice(pathMatchKeys.length).join('/')
);
}
// reduce the items of the query string. any query params may
// be overridden by path params
const queryParams = (queryString || '')
.split('&')
.reduce((result: *, item: string) => {
if (item !== '') {
const nextResult = result || {};
const [key, value] = item.split('=');
nextResult[key] = value;
return nextResult;
}
return result;
}, null);
// reduce the matched pieces of the path into the params
// of the route. `params` is null if there are no params.
/* $FlowFixMe */
const params = pathMatch
.slice(1)
.reduce((result: *, matchResult: *, i: number) => {
const key = pathMatchKeys[i];
if (key.asterisk || !key) {
return result;
}
const nextResult = result || {};
const paramName = key.name;
nextResult[paramName] = matchResult;
return nextResult;
}, queryParams);
return NavigationActions.navigate({
routeName: matchedRouteName,
...(params ? { params } : {}),
...(nestedAction ? { action: nestedAction } : {}),
});
},
getScreenOptions: createConfigGetter(
routeConfigs,
stackConfig.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};