Skip to content

Commit

Permalink
add support for an array of paths in <Route> and matchPath (#5889)
Browse files Browse the repository at this point in the history
  • Loading branch information
baronswindle authored and mjackson committed Oct 15, 2018
1 parent ef549ef commit a4d0064
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 23 deletions.
8 changes: 6 additions & 2 deletions packages/react-router/docs/api/Route.md
Expand Up @@ -133,14 +133,18 @@ This could also be useful for animations:

**Warning:** Both `<Route component>` and `<Route render>` take precedence over `<Route children>` so don't use more than one in the same `<Route>`.

## path: string
## path: string | string[]

Any valid URL path that [`path-to-regexp@^1.7.0`](https://github.com/pillarjs/path-to-regexp/tree/v1.7.0) understands.
Any valid URL path or array of paths that [`path-to-regexp@^1.7.0`](https://github.com/pillarjs/path-to-regexp/tree/v1.7.0) understands.

```jsx
<Route path="/users/:id" component={User} />
```

```jsx
<Route path={["/users/:id", "/profile/:id"]} component={User} />
```

Routes without a `path` _always_ match.

## exact: bool
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/docs/api/matchPath.md
Expand Up @@ -24,7 +24,7 @@ to the matching props `Route` accepts:

```js
{
path, // like /users/:id
path, // like /users/:id; either a single string or an array of strings
strict, // optional, defaults to false
exact; // optional, defaults to false
}
Expand Down
5 changes: 4 additions & 1 deletion packages/react-router/modules/Route.js
Expand Up @@ -130,7 +130,10 @@ if (__DEV__) {
component: PropTypes.func,
exact: PropTypes.bool,
location: PropTypes.object,
path: PropTypes.string,
path: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
render: PropTypes.func,
sensitive: PropTypes.bool,
strict: PropTypes.bool
Expand Down
77 changes: 77 additions & 0 deletions packages/react-router/modules/__tests__/Route-test.js
Expand Up @@ -98,6 +98,83 @@ describe("A <Route>", () => {
});
});

describe("with an array of paths", () => {
it("matches the first provided path", () => {
const node = document.createElement("div");
ReactDOM.render(
<MemoryRouter initialEntries={["/hello"]}>
<Route
path={["/hello", "/world"]}
render={() => <div>Hello World</div>}
/>
</MemoryRouter>,
node
);

expect(node.innerHTML).toContain("Hello World");
});

it("matches other provided paths", () => {
const node = document.createElement("div");
ReactDOM.render(
<MemoryRouter initialEntries={["/other", "/world"]} initialIndex={1}>
<Route
path={["/hello", "/world"]}
render={() => <div>Hello World</div>}
/>
</MemoryRouter>,
node
);

expect(node.innerHTML).toContain("Hello World");
});

it("provides the matched path as a string", () => {
const node = document.createElement("div");
ReactDOM.render(
<MemoryRouter initialEntries={["/other", "/world"]} initialIndex={1}>
<Route
path={["/hello", "/world"]}
render={({ match }) => <div>{match.path}</div>}
/>
</MemoryRouter>,
node
);

expect(node.innerHTML).toContain("/world");
});

it("doesn't remount when moving from one matching path to another", () => {
const node = document.createElement("div");
const history = createHistory();
const mount = jest.fn();
class MatchedRoute extends React.Component {
componentWillMount() {
mount();
}

render() {
return <div>Hello World</div>;
}
}
history.push("/hello");
ReactDOM.render(
<Router history={history}>
<Route path={["/hello", "/world"]} component={MatchedRoute} />
</Router>,
node
);

expect(mount).toHaveBeenCalledTimes(1);
expect(node.innerHTML).toContain("Hello World");

history.push("/world/somewhere/else");

expect(mount).toHaveBeenCalledTimes(1);
expect(node.innerHTML).toContain("Hello World");
});
});

describe("with a unicode path", () => {
it("is able to match", () => {
renderStrict(
Expand Down
30 changes: 30 additions & 0 deletions packages/react-router/modules/__tests__/matchPath-test.js
Expand Up @@ -33,6 +33,36 @@ describe("matchPath", () => {
});
});

describe("with an array of paths", () => {
it('return the correct url at "/elsewhere"', () => {
const path = ["/somewhere", "/elsewhere"];
const pathname = "/elsewhere";
const match = matchPath(pathname, { path });
expect(match.url).toBe("/elsewhere");
});

it('returns correct url at "/elsewhere/else"', () => {
const path = ["/somewhere", "/elsewhere"];
const pathname = "/elsewhere/else";
const match = matchPath(pathname, { path });
expect(match.url).toBe("/elsewhere");
});

it('returns correct url at "/elsewhere/else" with path "/" in array', () => {
const path = ["/somewhere", "/"];
const pathname = "/elsewhere/else";
const match = matchPath(pathname, { path });
expect(match.url).toBe("/");
});

it('returns correct url at "/somewhere" with path "/" in array', () => {
const path = ["/somewhere", "/"];
const pathname = "/somewhere";
const match = matchPath(pathname, { path });
expect(match.url).toBe("/somewhere");
});
});

describe("with sensitive path", () => {
it("returns non-sensitive url", () => {
const options = {
Expand Down
47 changes: 28 additions & 19 deletions packages/react-router/modules/matchPath.js
Expand Up @@ -30,25 +30,34 @@ function matchPath(pathname, options = {}) {

const { path, exact = false, strict = false, sensitive = false } = options;

const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = regexp.exec(pathname);

if (!match) return null;

const [url, ...values] = match;
const isExact = pathname === url;

if (exact && !isExact) return null;

return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
const paths = [].concat(path);

return paths.reduce((matched, path) => {
if (matched) return matched;
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);

if (!match) return null;

const [url, ...values] = match;
const isExact = pathname === url;

if (exact && !isExact) return null;

return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}

export default matchPath;

0 comments on commit a4d0064

Please sign in to comment.