Skip to content

Commit 2c28b9d

Browse files
committed
Add React Router v4 support
1 parent a7de1be commit 2c28b9d

File tree

5 files changed

+171
-168
lines changed

5 files changed

+171
-168
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@
7373
"lodash": "^4.14.0",
7474
"mocha": "^2.5.3",
7575
"react": "^15.2.1",
76+
"react-addons-test-utils": "^15.4.2",
7677
"react-bootstrap": "^0.30.0",
7778
"react-dom": "^15.2.1",
78-
"react-router": "^2.6.0",
79+
"react-router": "4.0.0-beta.5",
7980
"release-script": "^1.0.2",
8081
"rimraf": "^2.5.4",
8182
"shelljs": "^0.7.2",

src/IndexLinkContainer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import LinkContainer from './LinkContainer';
77
export default class IndexLinkContainer extends React.Component {
88
render() {
99
return (
10-
<LinkContainer {...this.props} onlyActiveOnIndex />
10+
<LinkContainer {...this.props} exact />
1111
);
1212
}
1313
}

src/LinkContainer.js

Lines changed: 88 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,43 @@
1-
// This is largely taken from react-router/lib/Link.
2-
3-
import React from 'react';
4-
5-
function isLeftClickEvent(event) {
6-
return event.button === 0;
7-
}
8-
9-
function isModifiedEvent(event) {
10-
return !!(
11-
event.metaKey ||
12-
event.altKey ||
13-
event.ctrlKey ||
14-
event.shiftKey
15-
);
16-
}
17-
18-
function createLocationDescriptor(to, query, hash, state) {
19-
if (query || hash || state) {
20-
return { pathname: to, query, hash, state };
21-
}
22-
23-
return to;
24-
}
25-
26-
const propTypes = {
27-
onlyActiveOnIndex: React.PropTypes.bool.isRequired,
28-
to: React.PropTypes.oneOfType([
29-
React.PropTypes.string,
30-
React.PropTypes.object,
31-
]).isRequired,
32-
query: React.PropTypes.string,
33-
hash: React.PropTypes.string,
34-
state: React.PropTypes.object,
35-
action: React.PropTypes.oneOf([
36-
'push',
37-
'replace',
38-
]).isRequired,
39-
onClick: React.PropTypes.func,
40-
active: React.PropTypes.bool,
41-
target: React.PropTypes.string,
42-
children: React.PropTypes.node.isRequired,
43-
};
1+
import React, { Component, PropTypes } from 'react';
2+
import { Route } from 'react-router';
3+
4+
const isModifiedEvent = (event) =>
5+
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
6+
7+
export default class LinkContainer extends Component {
8+
static contextTypes = {
9+
router: PropTypes.shape({
10+
push: PropTypes.func.isRequired,
11+
replace: PropTypes.func.isRequired,
12+
createHref: PropTypes.func.isRequired,
13+
}).isRequired,
14+
};
4415

45-
const contextTypes = {
46-
router: React.PropTypes.object,
47-
};
16+
static propTypes = {
17+
children: PropTypes.element.isRequired,
18+
onClick: PropTypes.func,
19+
target: PropTypes.string,
20+
replace: PropTypes.bool,
21+
to: PropTypes.oneOfType([
22+
PropTypes.string,
23+
PropTypes.object,
24+
]).isRequired,
25+
exact: PropTypes.bool,
26+
strict: PropTypes.bool,
27+
className: PropTypes.string,
28+
activeClassName: PropTypes.string,
29+
style: PropTypes.object,
30+
activeStyle: PropTypes.object,
31+
isActive: PropTypes.func,
32+
};
4833

49-
const defaultProps = {
50-
onlyActiveOnIndex: false,
51-
action: 'push',
52-
};
34+
static defaultProps = {
35+
replace: false,
36+
activeClassName: 'active',
37+
};
5338

54-
class LinkContainer extends React.Component {
55-
onClick = (event) => {
56-
const {
57-
to, query, hash, state, children, onClick, target, action,
58-
} = this.props;
39+
handleClick = (event) => {
40+
const { children, onClick } = this.props;
5941

6042
if (children.props.onClick) {
6143
children.props.onClick(event);
@@ -66,42 +48,63 @@ class LinkContainer extends React.Component {
6648
}
6749

6850
if (
69-
target ||
70-
event.defaultPrevented ||
71-
isModifiedEvent(event) ||
72-
!isLeftClickEvent(event)
51+
!event.defaultPrevented && // onClick prevented default
52+
event.button === 0 && // ignore right clicks
53+
!this.props.target && // let browser handle "target=_blank" etc.
54+
!isModifiedEvent(event) // ignore clicks with modifier keys
7355
) {
74-
return;
75-
}
56+
event.preventDefault();
7657

77-
event.preventDefault();
58+
const { router } = this.context;
59+
const { replace, to } = this.props;
7860

79-
this.context.router[action](
80-
createLocationDescriptor(to, query, hash, state)
81-
);
82-
};
61+
if (replace) {
62+
router.replace(to);
63+
} else {
64+
router.push(to);
65+
}
66+
}
67+
}
8368

8469
render() {
85-
const { router } = this.context;
86-
const { onlyActiveOnIndex, to, children, ...props } = this.props;
87-
88-
props.onClick = this.onClick;
89-
90-
// Ignore if rendered outside Router context; simplifies unit testing.
91-
if (router) {
92-
props.href = router.createHref(to);
70+
const {
71+
children,
72+
replace, // eslint-disable-line no-unused-vars
73+
to,
74+
exact,
75+
strict,
76+
activeClassName,
77+
className,
78+
activeStyle,
79+
style,
80+
isActive: getIsActive,
81+
...props,
82+
} = this.props;
9383

94-
if (props.active == null) {
95-
props.active = router.isActive(to, onlyActiveOnIndex);
96-
}
97-
}
84+
const href = this.context.router.createHref(
85+
typeof to === 'string' ? { pathname: to } : to
86+
);
9887

99-
return React.cloneElement(React.Children.only(children), props);
88+
return (
89+
<Route
90+
path={typeof to === 'object' ? to.pathname : to}
91+
exact={exact}
92+
strict={strict}
93+
children={({ location, match }) => {
94+
const isActive = !!(getIsActive ? getIsActive(match, location) : match);
95+
96+
return React.cloneElement(
97+
React.Children.only(children),
98+
{
99+
...props,
100+
className: (className || '') + (isActive ? activeClassName : ''),
101+
style: isActive ? { ...style, ...activeStyle } : style,
102+
href,
103+
onClick: this.handleClick,
104+
}
105+
);
106+
}}
107+
/>
108+
);
100109
}
101110
}
102-
103-
LinkContainer.propTypes = propTypes;
104-
LinkContainer.contextTypes = contextTypes;
105-
LinkContainer.defaultProps = defaultProps;
106-
107-
export default LinkContainer;

test/IndexLinkContainer.spec.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react';
2-
import ReactTestUtils from 'react/lib/ReactTestUtils';
2+
import ReactTestUtils from 'react-addons-test-utils';
33
import * as ReactBootstrap from 'react-bootstrap';
4-
import ReactDOM from 'react-dom';
5-
import { createMemoryHistory, IndexRoute, Route, Router } from 'react-router';
4+
import { findDOMNode } from 'react-dom';
5+
import { Route, MemoryRouter as Router } from 'react-router';
66

77
import IndexLinkContainer from '../src/IndexLinkContainer';
88

@@ -19,25 +19,24 @@ describe('IndexLinkContainer', () => {
1919
describe('active state', () => {
2020
function renderComponent(location) {
2121
const router = ReactTestUtils.renderIntoDocument(
22-
<Router history={createMemoryHistory(location)}>
23-
<Route
24-
path="/"
25-
component={() => (
26-
<IndexLinkContainer to="/">
27-
<Component>Root</Component>
28-
</IndexLinkContainer>
29-
)}
30-
>
31-
<IndexRoute />
32-
<Route path="foo" />
33-
</Route>
22+
<Router initialEntries={[location]}>
23+
<div>
24+
<Route
25+
path="/"
26+
render={() => (
27+
<IndexLinkContainer to="/">
28+
<Component>Root</Component>
29+
</IndexLinkContainer>
30+
)}
31+
/>
32+
</div>
3433
</Router>
3534
);
3635

3736
const component = ReactTestUtils.findRenderedComponentWithType(
3837
router, Component
3938
);
40-
return ReactDOM.findDOMNode(component);
39+
return findDOMNode(component);
4140
}
4241

4342
it('should be active on the index route', () => {

0 commit comments

Comments
 (0)