Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for exact path match before checking routes #3313

Merged
merged 1 commit into from
Apr 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions modules/__tests__/isActive-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ describe('isActive', function () {
})
})

// This test case is a bit odd. A route with path /parent/child/ won't
// match /parent/child because of the trailing slash mismatch. However,
// this doesn't matter in practice, since it only comes up if your
// isActive pattern has a trailing slash but your route pattern doesn't,
// which would be an utterly bizarre thing to do.

it('is active with trailing slash on pattern', function (done) {
render((
<Router history={createHistory('/parent/child')}>
Expand Down
101 changes: 55 additions & 46 deletions modules/isActive.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,40 @@ function deepEqual(a, b) {
return String(a) === String(b)
}

function paramsAreActive(paramNames, paramValues, activeParams) {
// FIXME: This doesn't work on repeated params in activeParams.
return paramNames.every(function (paramName, index) {
return String(paramValues[index]) === String(activeParams[paramName])
})
/**
* Returns true if the current pathname matches the supplied one, net of
* leading and trailing slash normalization. This is sufficient for an
* indexOnly route match.
*/
function pathIsActive(pathname, currentPathname) {
// Normalize leading slash for consistency. Leading slash on pathname has
// already been normalized in isActive. See caveat there.
if (currentPathname.charAt(0) !== '/') {
currentPathname = `/${currentPathname}`
}

// Normalize the end of both path names too. Maybe `/foo/` shouldn't show
// `/foo` as active, but in this case, we would already have failed the
// match.
if (pathname.charAt(pathname.length - 1) !== '/') {
pathname += '/'
}
if (currentPathname.charAt(currentPathname.length - 1) !== '/') {
currentPathname += '/'
}

return currentPathname === pathname
}

function getMatchingRouteIndex(pathname, activeRoutes, activeParams) {
/**
* Returns true if the given pathname matches the active routes and params.
*/
function routeIsActive(pathname, routes, params) {
let remainingPathname = pathname, paramNames = [], paramValues = []

for (let i = 0, len = activeRoutes.length; i < len; ++i) {
const route = activeRoutes[i]
// for...of would work here but it's probably slower post-transpilation.
for (let i = 0, len = routes.length; i < len; ++i) {
const route = routes[i]
const pattern = route.path || ''

if (pattern.charAt(0) === '/') {
Expand All @@ -58,49 +80,24 @@ function getMatchingRouteIndex(pathname, activeRoutes, activeParams) {
paramValues = []
}

if (remainingPathname !== null) {
if (remainingPathname !== null && pattern) {
const matched = matchPattern(pattern, remainingPathname)
remainingPathname = matched.remainingPathname
paramNames = [ ...paramNames, ...matched.paramNames ]
paramValues = [ ...paramValues, ...matched.paramValues ]
}

if (
remainingPathname === '' &&
route.path &&
paramsAreActive(paramNames, paramValues, activeParams)
)
return i
}

return null
}

/**
* Returns true if the given pathname matches the active routes
* and params.
*/
function routeIsActive(pathname, routes, params, indexOnly) {
// TODO: This is a bit ugly. It keeps around support for treating pathnames
// without preceding slashes as absolute paths, but possibly also works
// around the same quirks with basenames as in matchRoutes.
if (pathname.charAt(0) !== '/') {
pathname = `/${pathname}`
}

const i = getMatchingRouteIndex(pathname, routes, params)

if (i === null) {
// No match.
return false
} else if (!indexOnly) {
// Any match is good enough.
return true
if (remainingPathname === '') {
// We have an exact match on the route. Just check that all the params
// match.
// FIXME: This doesn't work on repeated params.
return paramNames.every((paramName, index) => (
String(paramValues[index]) === String(params[paramName])
))
}
}
}

// If any remaining routes past the match index have paths, then we can't
// be on the index route.
return routes.slice(i + 1).every(route => !route.path)
return false
}

/**
Expand All @@ -127,8 +124,20 @@ export default function isActive(
if (currentLocation == null)
return false

if (!routeIsActive(pathname, routes, params, indexOnly))
return false
// TODO: This is a bit ugly. It keeps around support for treating pathnames
// without preceding slashes as absolute paths, but possibly also works
// around the same quirks with basenames as in matchRoutes.
if (pathname.charAt(0) !== '/') {
pathname = `/${pathname}`
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're doing this twice, we can optimize a tiny bit by just moving this up to isActive itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, given the similarity with currentPathname as well, you can just do a method extraction here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be overkill to do that – this snippet is short/simple enough that I'm not too worried about duplicating it unnecessarily.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a nitpick. Do what you want :)


if (!pathIsActive(pathname, currentLocation.pathname)) {
// The path check is necessary and sufficient for indexOnly, but otherwise
// we still need to check the routes.
if (indexOnly || !routeIsActive(pathname, routes, params)) {
return false
}
}

return queryIsActive(query, currentLocation.query)
}