Skip to content

Commit

Permalink
Add withRouter
Browse files Browse the repository at this point in the history
- Use withRouter in createConnectedLink
- Switch order of arguments to isActive
- Use custom pattern for injecting additional props from containers
  • Loading branch information
taion committed Nov 1, 2016
1 parent 8ac2f60 commit 3066ed5
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 62 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"lodash": "^4.16.4",
"path-to-regexp": "^1.6.0",
"react-prop-types": "^0.4.0",
"react-redux": "^4.4.5",
"react-redux": "^5.0.0-beta.3",
"redux": "^3.6.0",
"warning": "^3.0.0"
},
Expand Down
17 changes: 6 additions & 11 deletions src/BaseLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,20 @@ const propTypes = {
activeClassName: React.PropTypes.string,
activeStyle: React.PropTypes.object,
activePropName: React.PropTypes.string,
router: routerShape.isRequired,
exact: React.PropTypes.bool.isRequired,
target: React.PropTypes.string,
onClick: React.PropTypes.func,
};

const contextTypes = {
router: routerShape.isRequired,
};

const defaultProps = {
Component: 'a',
exact: false,
};

class BaseLink extends React.Component {
onClick = (event) => {
const { onClick, target, to } = this.props;
const { onClick, target, router, to } = this.props;

if (onClick) {
onClick(event);
Expand All @@ -55,7 +52,7 @@ class BaseLink extends React.Component {
// FIXME: When clicking on a link to the same location in the browser, the
// actual becomes a replace rather than a push. We may want the same
// handling – perhaps implemented in the Farce protocol.
this.context.router.push(to);
router.push(to);
};

render() {
Expand All @@ -66,15 +63,14 @@ class BaseLink extends React.Component {
activeClassName,
activeStyle,
activePropName,
router,
exact,
...props
} = this.props;

const { router } = this.context;

if (activeClassName || activeStyle || activePropName) {
const toLocation = router.createLocation(to);
const active = router.isActive(toLocation, match, { exact });
const active = router.isActive(match, toLocation, { exact });

if (active) {
if (activeClassName) {
Expand All @@ -96,14 +92,13 @@ class BaseLink extends React.Component {
<Component
{...props}
href={router.createHref(to)}
onClick={this.onClick}
onClick={this.onClick} // This overrides props.onClick.
/>
);
}
}

BaseLink.propTypes = propTypes;
BaseLink.contextTypes = contextTypes;
BaseLink.defaultProps = defaultProps;

export default BaseLink;
2 changes: 1 addition & 1 deletion src/Link.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import createConnectedLink from './createConnectedLink';

export default createConnectedLink({});
export default createConnectedLink();
10 changes: 5 additions & 5 deletions src/Matcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,18 @@ export default class Matcher {
return pattern.charAt(0) === '/' ? pattern : `/${pattern}`;
}

isActive(location, { location: matchLocation }, { exact } = {}) {
isActive({ location: matchLocation }, location, { exact } = {}) {
return (
this.isPathnameActive(
location.pathname, matchLocation.pathname, exact,
matchLocation.pathname, location.pathname, exact,
) &&
this.isQueryActive(
location.query, matchLocation.query,
matchLocation.query, location.query,
)
);
}

isPathnameActive(pathname, matchPathname, exact) {
isPathnameActive(matchPathname, pathname, exact) {
if (exact) {
return pathname === matchPathname;
}
Expand All @@ -103,7 +103,7 @@ export default class Matcher {
return matchPathname.indexOf(pathname) === 0;
}

isQueryActive(query, matchQuery) {
isQueryActive(matchQuery, query) {
if (!query) {
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/createBaseRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export default function createBaseRouter({ routeConfig, matcher, render }) {
async resolveMatch() {
const currentMatchIndex = this.matchIndex;
const { match, matchContext, resolveElements } = this.props;

const routes = getRoutes(routeConfig, match);

const augmentedMatch = {
...match,
routes,
Expand Down
19 changes: 5 additions & 14 deletions src/createConnectedLink.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import { connect } from 'react-redux';

import BaseLink from './BaseLink';
import createWithRouter from './createWithRouter';
import defaultWithRouter from './withRouter';

export default function createConnectedLink({
getMatch = ({ match }) => match,
}) {
return connect(
state => ({ match: getMatch(state) }),
null,
(stateProps, dispatchProps, ownProps) => ({
...ownProps,
...stateProps,
// We don't want dispatch here.
}),
)(BaseLink);
export default function createConnectedLink(options) {
const withRouter = options ? createWithRouter(options) : defaultWithRouter;
return withRouter(BaseLink);
}
50 changes: 25 additions & 25 deletions src/createConnectedRouter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import FarceActions from 'farce/lib/Actions';
import React from 'react';
import { connect } from 'react-redux';

import createBaseRouter from './createBaseRouter';
Expand All @@ -8,39 +7,40 @@ export default function createConnectedRouter({
getMatch = ({ match }) => match,
...options
}) {
// createHref, createLocation, and isActive are taken directly from the store
// to avoid potential issues with middlewares in the chain messing with the
// return value from dispatch.
const propTypes = {
store: React.PropTypes.shape({
farce: React.PropTypes.shape({
createHref: React.PropTypes.func.isRequired,
createLocation: React.PropTypes.func.isRequired,
}).isRequired,
found: React.PropTypes.shape({
isActive: React.PropTypes.func.isRequired,
}).isRequired,
}).isRequired,
};

const ConnectedRouter = connect(
state => ({ match: getMatch(state) }),
{
push: FarceActions.push,
replace: FarceActions.replace,
go: FarceActions.go,
},
(stateProps, dispatchProps, { store, ...ownProps }) => ({
...ownProps,
...stateProps,
...dispatchProps,
createHref: store.farce.createHref,
createLocation: store.farce.createLocation,
isActive: store.found.isActive,
}),
)(createBaseRouter(options));

ConnectedRouter.propTypes = propTypes;
// This implementation is very messy, but it provides the cleanest API to get
// these methods into the base router from the store, since they're already
// on the store context.

// Overwriting the method instead of extending the class is used to avoid
// issues with compatibility on IE <= 10.
const baseAddExtraProps = ConnectedRouter.prototype.addExtraProps;

function addExtraProps(props) {
// It's safe to read from the context because these won't change.
const { farce, found } = this.props.store || this.context.store;

return {
...baseAddExtraProps.call(this, props),

// Take createHref, createLocation, and isActive directly from the store
// to avoid potential issues with middlewares in the chain messing with
// the return value from dispatch.
createHref: farce.createHref,
createLocation: farce.createLocation,
isActive: found.isActive,
};
}

ConnectedRouter.prototype.addExtraProps = addExtraProps;

return ConnectedRouter;
}
5 changes: 1 addition & 4 deletions src/createFarceRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ export default function createFarceRouter({
render() {
return (
<Provider store={this.store}>
<ConnectedRouter
{...this.props}
store={this.store}
/>
<ConnectedRouter {...this.props} />
</Provider>
);
}
Expand Down
52 changes: 52 additions & 0 deletions src/createWithRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { connect } from 'react-redux';

import { routerShape } from './PropTypes';

const routerContextTypes = {
router: routerShape.isRequired,
};

export default function createWithRouter({
getMatch = ({ match }) => match,
}) {
const withMatch = connect(
state => ({ match: getMatch(state) }),
null,
(stateProps, dispatchProps, ownProps) => ({
...ownProps,
...stateProps,
// We don't want dispatch here.
}),
);

return function withRouter(Component) {
const ConnectedComponent = withMatch(Component);

// Yes, this is pretty gross. It's the simplest way to inject router as
// a prop without adding yet another wrapper component, though.

ConnectedComponent.contextTypes = {
...ConnectedComponent.contextTypes,
...routerContextTypes,
};

// Overwriting the method instead of extending the class is used to avoid
// issues with compatibility on IE <= 10.
const baseAddExtraProps = ConnectedComponent.prototype.addExtraProps;

function addExtraProps(props) {
return {
...baseAddExtraProps.call(this, props),

// It's safe to read from the context because the router context
// methods should never change. With the default implementation, they
// in fact can't change.
router: this.context.router,
};
}

ConnectedComponent.prototype.addExtraProps = addExtraProps;

return ConnectedComponent;
};
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export createElements from './createElements';
export createFarceRouter from './createFarceRouter';
export createMatchEnhancer from './createMatchEnhancer';
export createRender from './createRender';
export createWithRouter from './createWithRouter';
export ElementsRenderer from './ElementsRenderer';
export getRoutes from './getRoutes';
export HttpError from './HttpError';
Expand All @@ -19,3 +20,4 @@ export Redirect from './Redirect';
export RedirectException from './RedirectException';
export resolveElements from './resolveElements';
export ResolverUtils from './ResolverUtils';
export withRouter from './withRouter';
3 changes: 3 additions & 0 deletions src/withRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createWithRouter from './createWithRouter';

export default createWithRouter({});

0 comments on commit 3066ed5

Please sign in to comment.