Skip to content

Commit bb802cd

Browse files
smashercosmotimdorr
authored andcommitted
Make Link and NavLink components accept "to" property as a function (#5368)
1 parent 1ad731a commit bb802cd

File tree

7 files changed

+164
-31
lines changed

7 files changed

+164
-31
lines changed
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
{
22
"esm/react-router-dom.js": {
3-
"bundled": 8362,
4-
"minified": 5058,
5-
"gzipped": 1651,
3+
"bundled": 8874,
4+
"minified": 5312,
5+
"gzipped": 1711,
66
"treeshaked": {
77
"rollup": {
8-
"code": 453,
8+
"code": 508,
99
"import_statements": 417
1010
},
1111
"webpack": {
12-
"code": 1661
12+
"code": 1800
1313
}
1414
}
1515
},
1616
"umd/react-router-dom.js": {
17-
"bundled": 159395,
18-
"minified": 56787,
19-
"gzipped": 16387
17+
"bundled": 159933,
18+
"minified": 56923,
19+
"gzipped": 16433
2020
},
2121
"umd/react-router-dom.min.js": {
22-
"bundled": 96151,
23-
"minified": 33747,
24-
"gzipped": 9951
22+
"bundled": 96671,
23+
"minified": 33875,
24+
"gzipped": 9980
2525
}
2626
}

packages/react-router-dom/docs/api/Link.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ An object that can have any of the following properties:
3636
/>
3737
```
3838

39+
## to: function
40+
41+
A function to which current location is passed as an argument and which should return location representation as a string or as an object
42+
43+
```jsx
44+
<Link to={location => ({ ...location, pathname: "/courses" })} />
45+
```
46+
47+
```jsx
48+
<Link to={location => `${location.pathname}?sort=name`} />
49+
```
50+
3951
## replace: bool
4052

4153
When `true`, clicking the link will replace the current entry in the history stack instead of adding a new one.

packages/react-router-dom/modules/Link.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from "react";
22
import { __RouterContext as RouterContext } from "react-router";
3-
import { createLocation } from "history";
43
import PropTypes from "prop-types";
54
import invariant from "tiny-invariant";
5+
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";
66

77
function isModifiedEvent(event) {
88
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
@@ -12,7 +12,7 @@ function isModifiedEvent(event) {
1212
* The public API for rendering a history-aware <a>.
1313
*/
1414
class Link extends React.Component {
15-
handleClick(event, history) {
15+
handleClick(event, context) {
1616
try {
1717
if (this.props.onClick) this.props.onClick(event);
1818
} catch (ex) {
@@ -28,9 +28,13 @@ class Link extends React.Component {
2828
) {
2929
event.preventDefault();
3030

31-
const method = this.props.replace ? history.replace : history.push;
31+
const location = resolveToLocation(this.props.to, context.location);
3232

33-
method(this.props.to);
33+
const method = this.props.replace
34+
? context.history.replace
35+
: context.history.push;
36+
37+
method(location);
3438
}
3539
}
3640

@@ -42,16 +46,17 @@ class Link extends React.Component {
4246
{context => {
4347
invariant(context, "You should not use <Link> outside a <Router>");
4448

45-
const location =
46-
typeof to === "string"
47-
? createLocation(to, null, null, context.location)
48-
: to;
49+
const location = normalizeToLocation(
50+
resolveToLocation(to, context.location),
51+
context.location
52+
);
53+
4954
const href = location ? context.history.createHref(location) : "";
5055

5156
return (
5257
<a
5358
{...rest}
54-
onClick={event => this.handleClick(event, context.history)}
59+
onClick={event => this.handleClick(event, context)}
5560
href={href}
5661
ref={innerRef}
5762
/>
@@ -63,7 +68,11 @@ class Link extends React.Component {
6368
}
6469

6570
if (__DEV__) {
66-
const toType = PropTypes.oneOfType([PropTypes.string, PropTypes.object]);
71+
const toType = PropTypes.oneOfType([
72+
PropTypes.string,
73+
PropTypes.object,
74+
PropTypes.func
75+
]);
6776
const innerRefType = PropTypes.oneOfType([
6877
PropTypes.string,
6978
PropTypes.func,

packages/react-router-dom/modules/NavLink.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from "react";
22
import { __RouterContext as RouterContext, matchPath } from "react-router";
33
import PropTypes from "prop-types";
4-
import Link from "./Link";
54
import invariant from "tiny-invariant";
5+
import Link from "./Link";
6+
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";
67

78
function joinClassnames(...classnames) {
89
return classnames.filter(i => i).join(" ");
@@ -24,19 +25,22 @@ function NavLink({
2425
to,
2526
...rest
2627
}) {
27-
const path = typeof to === "object" ? to.pathname : to;
28-
29-
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
30-
const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
31-
3228
return (
3329
<RouterContext.Consumer>
3430
{context => {
3531
invariant(context, "You should not use <NavLink> outside a <Router>");
3632

37-
const pathToMatch = locationProp
38-
? locationProp.pathname
39-
: context.location.pathname;
33+
const currentLocation = locationProp || context.location;
34+
const { pathname: pathToMatch } = currentLocation;
35+
const toLocation = normalizeToLocation(
36+
resolveToLocation(to, currentLocation),
37+
currentLocation
38+
);
39+
const { pathname: path } = toLocation;
40+
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
41+
const escapedPath =
42+
path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
43+
4044
const match = escapedPath
4145
? matchPath(pathToMatch, { path: escapedPath, exact, strict })
4246
: null;
@@ -54,7 +58,7 @@ function NavLink({
5458
aria-current={(isActive && ariaCurrent) || null}
5559
className={className}
5660
style={style}
57-
to={to}
61+
to={toLocation}
5862
{...rest}
5963
/>
6064
);

packages/react-router-dom/modules/__tests__/Link-test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,34 @@ describe("A <Link>", () => {
7575
expect(a.getAttribute("href")).toEqual("/the/path?the=query#the-hash");
7676
});
7777

78+
it("accepts an object returning function `to` prop", () => {
79+
const to = location => ({ ...location, search: "foo=bar" });
80+
81+
renderStrict(
82+
<MemoryRouter initialEntries={["/hello"]}>
83+
<Link to={to}>link</Link>
84+
</MemoryRouter>,
85+
node
86+
);
87+
88+
const a = node.querySelector("a");
89+
expect(a.getAttribute("href")).toEqual("/hello?foo=bar");
90+
});
91+
92+
it("accepts a string returning function `to` prop", () => {
93+
const to = location => `${location.pathname}?foo=bar`;
94+
95+
ReactDOM.render(
96+
<MemoryRouter initialEntries={["/hello"]}>
97+
<Link to={to}>link</Link>
98+
</MemoryRouter>,
99+
node
100+
);
101+
102+
const a = node.querySelector("a");
103+
expect(a.getAttribute("href")).toEqual("/hello?foo=bar");
104+
});
105+
78106
describe("with no pathname", () => {
79107
it("resolves using the current location", () => {
80108
renderStrict(
@@ -218,6 +246,42 @@ describe("A <Link>", () => {
218246
expect(memoryHistory.push).toBeCalledWith(to);
219247
});
220248

249+
it("calls onClick eventhandler and history.push with function `to` prop", () => {
250+
const memoryHistoryFoo = createMemoryHistory({
251+
initialEntries: ["/foo"]
252+
});
253+
memoryHistoryFoo.push = jest.fn();
254+
const clickHandler = jest.fn();
255+
let to = null;
256+
const toFn = location => {
257+
to = {
258+
...location,
259+
pathname: "hello",
260+
search: "world"
261+
};
262+
return to;
263+
};
264+
265+
renderStrict(
266+
<Router history={memoryHistoryFoo}>
267+
<Link to={toFn} onClick={clickHandler}>
268+
link
269+
</Link>
270+
</Router>,
271+
node
272+
);
273+
274+
const a = node.querySelector("a");
275+
ReactTestUtils.Simulate.click(a, {
276+
defaultPrevented: false,
277+
button: 0
278+
});
279+
280+
expect(clickHandler).toBeCalledTimes(1);
281+
expect(memoryHistoryFoo.push).toBeCalledTimes(1);
282+
expect(memoryHistoryFoo.push).toBeCalledWith(to);
283+
});
284+
221285
it("does not call history.push on right click", () => {
222286
const to = "/the/path?the=query#the-hash";
223287

packages/react-router-dom/modules/__tests__/NavLink-test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ describe("A <NavLink>", () => {
2525
expect(a.className).toContain("active");
2626
});
2727

28+
it("applies its default activeClassName with function `to` prop", () => {
29+
renderStrict(
30+
<MemoryRouter initialEntries={["/pizza"]}>
31+
<NavLink to={location => ({ ...location, pathname: "/pizza" })}>
32+
Pizza!
33+
</NavLink>
34+
</MemoryRouter>,
35+
node
36+
);
37+
38+
const a = node.querySelector("a");
39+
40+
expect(a.className).toContain("active");
41+
});
42+
2843
it("applies a custom activeClassName instead of the default", () => {
2944
renderStrict(
3045
<MemoryRouter initialEntries={["/pizza"]}>
@@ -472,6 +487,25 @@ describe("A <NavLink>", () => {
472487
expect(a.className).toContain("active");
473488
});
474489

490+
it("is passed as an argument to function `to` prop", () => {
491+
renderStrict(
492+
<MemoryRouter initialEntries={["/pizza"]}>
493+
<NavLink
494+
to={location => location}
495+
activeClassName="selected"
496+
location={{ pathname: "/pasta" }}
497+
>
498+
Pasta!
499+
</NavLink>
500+
</MemoryRouter>,
501+
node
502+
);
503+
504+
const a = node.querySelector("a");
505+
expect(a.className).not.toContain("active");
506+
expect(a.className).toContain("selected");
507+
});
508+
475509
it("is not overwritten by the current location", () => {
476510
renderStrict(
477511
<MemoryRouter initialEntries={["/pasta"]}>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createLocation } from "history";
2+
3+
export const resolveToLocation = (to, currentLocation) =>
4+
typeof to === "function" ? to(currentLocation) : to;
5+
6+
export const normalizeToLocation = (to, currentLocation) => {
7+
return typeof to === "string"
8+
? createLocation(to, null, null, currentLocation)
9+
: to;
10+
};

0 commit comments

Comments
 (0)