react-router
Version:
A complete routing library for React.js
256 lines (221 loc) • 7.49 kB
JavaScript
import invariant from 'invariant';
import { createRoutes } from './RouteUtils';
import { getParamNames, matchPattern, stripLeadingSlashes } from './URLUtils';
import { loopAsync, mapAsync } from './AsyncUtils';
function getChildRoutes(route, locationState, callback) {
if (route.childRoutes) {
callback(null, route.childRoutes);
} else if (route.getChildRoutes) {
route.getChildRoutes(locationState, callback);
} else {
callback();
}
}
function getIndexRoute(route, locationState, callback) {
if (route.indexRoute) {
callback(null, route.indexRoute);
} else if (route.getIndexRoute) {
route.getIndexRoute(callback, locationState);
} else {
callback();
}
}
function assignParams(params, paramNames, paramValues) {
return paramNames.reduceRight(function (params, paramName, index) {
var paramValue = paramValues[index];
if (Array.isArray(params[paramName])) {
params[paramName].unshift(paramValue);
} else if (paramName in params) {
params[paramName] = [ paramValue, params[paramName] ];
} else {
params[paramName] = paramValue;
}
return params;
}, params);
}
function createParams(paramNames, paramValues) {
return assignParams({}, paramNames, paramValues);
}
function matchRouteDeep(route, pathname, locationState, callback) {
var { remainingPathname, paramNames, paramValues } = matchPattern(route.path, pathname);
var isExactMatch = remainingPathname === '';
if (isExactMatch && route.path) {
var params = createParams(paramNames, paramValues);
var branch = [ route ];
getIndexRoute(route, locationState, function (error, indexRoute) {
if (error) {
callback(error);
} else {
if (indexRoute)
branch.push(indexRoute);
callback(null, { params, branch });
}
});
} else if (remainingPathname != null) {
// This route matched at least some of the path.
getChildRoutes(route, locationState, function (error, childRoutes) {
if (error) {
callback(error);
} else if (childRoutes) {
// Check the child routes to see if any of them match.
matchRoutes(childRoutes, remainingPathname, locationState, function (error, match) {
if (error) {
callback(error);
} else if (match) {
// A child route matched! Augment the match and pass it up the stack.
assignParams(match.params, paramNames, paramValues);
match.branch.unshift(route);
callback(null, match);
} else {
callback();
}
});
} else {
callback();
}
});
} else {
callback();
}
}
function matchRoutes(routes, pathname, locationState, callback) {
routes = createRoutes(routes);
loopAsync(routes.length, function (index, next, done) {
matchRouteDeep(routes[index], pathname, locationState, function (error, match) {
if (error || match) {
done(error, match);
} else {
next();
}
});
}, callback);
}
/**
* Asynchronously matches the given location to a set of routes and calls
* callback(error, state) when finished. The state object may have the
* following properties:
*
* - branch An array of routes that matched, in hierarchical order
* - params An object of URL parameters
*
* Note: This operation may return synchronously if no routes have an
* asynchronous getChildRoutes method.
*/
export function getState(routes, location, callback) {
matchRoutes(routes, stripLeadingSlashes(location.pathname), location.state, callback);
}
function routeParamsChanged(route, prevState, nextState) {
if (!route.path)
return false;
var paramNames = getParamNames(route.path);
return paramNames.some(function (paramName) {
return prevState.params[paramName] !== nextState.params[paramName];
});
}
/**
* Runs a diff on the two router states and returns an array of two
* arrays: 1) the routes that we are leaving, starting with the leaf
* route and 2) the routes that we are entering, ending with the leaf
* route.
*/
function computeDiff(prevState, nextState) {
var fromRoutes = prevState && prevState.branch;
var toRoutes = nextState.branch;
var leavingRoutes, enteringRoutes;
if (fromRoutes) {
leavingRoutes = fromRoutes.filter(function (route) {
return toRoutes.indexOf(route) === -1 || routeParamsChanged(route, prevState, nextState);
});
// onLeave hooks start at the leaf route.
leavingRoutes.reverse();
enteringRoutes = toRoutes.filter(function (route) {
return fromRoutes.indexOf(route) === -1 || leavingRoutes.indexOf(route) !== -1;
});
} else {
leavingRoutes = [];
enteringRoutes = toRoutes;
}
return [
leavingRoutes,
enteringRoutes
];
}
export function createTransitionHook(fn, context) {
return function (nextState, transition, callback) {
if (fn.length > 2) {
fn.call(context, nextState, transition, callback);
} else {
// Assume fn executes synchronously and
// automatically call the callback for them.
fn.call(context, nextState, transition);
callback();
}
};
}
function getTransitionHooksFromRoutes(routes, hookName) {
return routes.reduce(function (hooks, route) {
if (route[hookName])
hooks.push(createTransitionHook(route[hookName], route));
return hooks;
}, []);
}
/**
* Compiles and returns an array of transition hook functions that
* should be called before we transition to a new state. Transition
* hook signatures are:
*
* - route.onLeave(nextState, transition[, callback ])
* - route.onEnter(nextState, transition[, callback ])
*
* Transition hooks run in order from the leaf route in the branch
* we're leaving, up the tree to the common parent route, and back
* down the branch we're entering to the leaf route.
*
* If a transition hook needs to execute asynchronously it may have
* a 3rd argument that it should call when it is finished. Otherwise
* the transition executes synchronously.
*/
export function getTransitionHooks(prevState, nextState) {
var [ leavingRoutes, enteringRoutes ] = computeDiff(prevState, nextState);
var hooks = getTransitionHooksFromRoutes(leavingRoutes, 'onLeave');
hooks.push.apply(
hooks,
getTransitionHooksFromRoutes(enteringRoutes, 'onEnter')
);
return hooks;
}
function getComponentsForRoute(route, callback) {
if (route.component || route.components) {
callback(null, route.component || route.components);
} else if (route.getComponents) {
route.getComponents(callback);
} else {
callback();
}
}
/**
* Asynchronously fetches all components needed for the given router
* state and calls callback(error, components) when finished.
*
* Note: This operation may return synchronously if no routes have an
* asynchronous getComponents method.
*/
export function getComponents(routes, callback) {
mapAsync(routes, function (route, index, callback) {
getComponentsForRoute(route, callback);
}, callback);
}
/**
* Extracts an object of params the given route cares about from
* the given params object.
*/
export function getRouteParams(route, params) {
var routeParams = {};
if (!route.path)
return routeParams;
var paramNames = getParamNames(route.path);
for (var p in params)
if (params.hasOwnProperty(p) && paramNames.indexOf(p) !== -1)
routeParams[p] = params[p];
return routeParams;
}