Skip to content

Commit

Permalink
Make Link and NavLink components accept "to" property as a function (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
smashercosmo authored and timdorr committed Jun 12, 2019
1 parent 1ad731a commit bb802cd
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 31 deletions.
22 changes: 11 additions & 11 deletions packages/react-router-dom/.size-snapshot.json
@@ -1,26 +1,26 @@
{
"esm/react-router-dom.js": {
"bundled": 8362,
"minified": 5058,
"gzipped": 1651,
"bundled": 8874,
"minified": 5312,
"gzipped": 1711,
"treeshaked": {
"rollup": {
"code": 453,
"code": 508,
"import_statements": 417
},
"webpack": {
"code": 1661
"code": 1800
}
}
},
"umd/react-router-dom.js": {
"bundled": 159395,
"minified": 56787,
"gzipped": 16387
"bundled": 159933,
"minified": 56923,
"gzipped": 16433
},
"umd/react-router-dom.min.js": {
"bundled": 96151,
"minified": 33747,
"gzipped": 9951
"bundled": 96671,
"minified": 33875,
"gzipped": 9980
}
}
12 changes: 12 additions & 0 deletions packages/react-router-dom/docs/api/Link.md
Expand Up @@ -36,6 +36,18 @@ An object that can have any of the following properties:
/>
```

## to: function

A function to which current location is passed as an argument and which should return location representation as a string or as an object

```jsx
<Link to={location => ({ ...location, pathname: "/courses" })} />
```

```jsx
<Link to={location => `${location.pathname}?sort=name`} />
```

## replace: bool

When `true`, clicking the link will replace the current entry in the history stack instead of adding a new one.
Expand Down
29 changes: 19 additions & 10 deletions packages/react-router-dom/modules/Link.js
@@ -1,8 +1,8 @@
import React from "react";
import { __RouterContext as RouterContext } from "react-router";
import { createLocation } from "history";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";

function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
Expand All @@ -12,7 +12,7 @@ function isModifiedEvent(event) {
* The public API for rendering a history-aware <a>.
*/
class Link extends React.Component {
handleClick(event, history) {
handleClick(event, context) {
try {
if (this.props.onClick) this.props.onClick(event);
} catch (ex) {
Expand All @@ -28,9 +28,13 @@ class Link extends React.Component {
) {
event.preventDefault();

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

method(this.props.to);
const method = this.props.replace
? context.history.replace
: context.history.push;

method(location);
}
}

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

const location =
typeof to === "string"
? createLocation(to, null, null, context.location)
: to;
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);

const href = location ? context.history.createHref(location) : "";

return (
<a
{...rest}
onClick={event => this.handleClick(event, context.history)}
onClick={event => this.handleClick(event, context)}
href={href}
ref={innerRef}
/>
Expand All @@ -63,7 +68,11 @@ class Link extends React.Component {
}

if (__DEV__) {
const toType = PropTypes.oneOfType([PropTypes.string, PropTypes.object]);
const toType = PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.func
]);
const innerRefType = PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
Expand Down
24 changes: 14 additions & 10 deletions packages/react-router-dom/modules/NavLink.js
@@ -1,8 +1,9 @@
import React from "react";
import { __RouterContext as RouterContext, matchPath } from "react-router";
import PropTypes from "prop-types";
import Link from "./Link";
import invariant from "tiny-invariant";
import Link from "./Link";
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";

function joinClassnames(...classnames) {
return classnames.filter(i => i).join(" ");
Expand All @@ -24,19 +25,22 @@ function NavLink({
to,
...rest
}) {
const path = typeof to === "object" ? to.pathname : to;

// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");

return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <NavLink> outside a <Router>");

const pathToMatch = locationProp
? locationProp.pathname
: context.location.pathname;
const currentLocation = locationProp || context.location;
const { pathname: pathToMatch } = currentLocation;
const toLocation = normalizeToLocation(
resolveToLocation(to, currentLocation),
currentLocation
);
const { pathname: path } = toLocation;
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath =
path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");

const match = escapedPath
? matchPath(pathToMatch, { path: escapedPath, exact, strict })
: null;
Expand All @@ -54,7 +58,7 @@ function NavLink({
aria-current={(isActive && ariaCurrent) || null}
className={className}
style={style}
to={to}
to={toLocation}
{...rest}
/>
);
Expand Down
64 changes: 64 additions & 0 deletions packages/react-router-dom/modules/__tests__/Link-test.js
Expand Up @@ -75,6 +75,34 @@ describe("A <Link>", () => {
expect(a.getAttribute("href")).toEqual("/the/path?the=query#the-hash");
});

it("accepts an object returning function `to` prop", () => {
const to = location => ({ ...location, search: "foo=bar" });

renderStrict(
<MemoryRouter initialEntries={["/hello"]}>
<Link to={to}>link</Link>
</MemoryRouter>,
node
);

const a = node.querySelector("a");
expect(a.getAttribute("href")).toEqual("/hello?foo=bar");
});

it("accepts a string returning function `to` prop", () => {
const to = location => `${location.pathname}?foo=bar`;

ReactDOM.render(
<MemoryRouter initialEntries={["/hello"]}>
<Link to={to}>link</Link>
</MemoryRouter>,
node
);

const a = node.querySelector("a");
expect(a.getAttribute("href")).toEqual("/hello?foo=bar");
});

describe("with no pathname", () => {
it("resolves using the current location", () => {
renderStrict(
Expand Down Expand Up @@ -218,6 +246,42 @@ describe("A <Link>", () => {
expect(memoryHistory.push).toBeCalledWith(to);
});

it("calls onClick eventhandler and history.push with function `to` prop", () => {
const memoryHistoryFoo = createMemoryHistory({
initialEntries: ["/foo"]
});
memoryHistoryFoo.push = jest.fn();
const clickHandler = jest.fn();
let to = null;
const toFn = location => {
to = {
...location,
pathname: "hello",
search: "world"
};
return to;
};

renderStrict(
<Router history={memoryHistoryFoo}>
<Link to={toFn} onClick={clickHandler}>
link
</Link>
</Router>,
node
);

const a = node.querySelector("a");
ReactTestUtils.Simulate.click(a, {
defaultPrevented: false,
button: 0
});

expect(clickHandler).toBeCalledTimes(1);
expect(memoryHistoryFoo.push).toBeCalledTimes(1);
expect(memoryHistoryFoo.push).toBeCalledWith(to);
});

it("does not call history.push on right click", () => {
const to = "/the/path?the=query#the-hash";

Expand Down
34 changes: 34 additions & 0 deletions packages/react-router-dom/modules/__tests__/NavLink-test.js
Expand Up @@ -25,6 +25,21 @@ describe("A <NavLink>", () => {
expect(a.className).toContain("active");
});

it("applies its default activeClassName with function `to` prop", () => {
renderStrict(
<MemoryRouter initialEntries={["/pizza"]}>
<NavLink to={location => ({ ...location, pathname: "/pizza" })}>
Pizza!
</NavLink>
</MemoryRouter>,
node
);

const a = node.querySelector("a");

expect(a.className).toContain("active");
});

it("applies a custom activeClassName instead of the default", () => {
renderStrict(
<MemoryRouter initialEntries={["/pizza"]}>
Expand Down Expand Up @@ -472,6 +487,25 @@ describe("A <NavLink>", () => {
expect(a.className).toContain("active");
});

it("is passed as an argument to function `to` prop", () => {
renderStrict(
<MemoryRouter initialEntries={["/pizza"]}>
<NavLink
to={location => location}
activeClassName="selected"
location={{ pathname: "/pasta" }}
>
Pasta!
</NavLink>
</MemoryRouter>,
node
);

const a = node.querySelector("a");
expect(a.className).not.toContain("active");
expect(a.className).toContain("selected");
});

it("is not overwritten by the current location", () => {
renderStrict(
<MemoryRouter initialEntries={["/pasta"]}>
Expand Down
10 changes: 10 additions & 0 deletions packages/react-router-dom/modules/utils/locationUtils.js
@@ -0,0 +1,10 @@
import { createLocation } from "history";

export const resolveToLocation = (to, currentLocation) =>
typeof to === "function" ? to(currentLocation) : to;

export const normalizeToLocation = (to, currentLocation) => {
return typeof to === "string"
? createLocation(to, null, null, currentLocation)
: to;
};

0 comments on commit bb802cd

Please sign in to comment.