Skip to content
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
22 changes: 11 additions & 11 deletions packages/react-router-dom/.size-snapshot.json
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
};