Skip to content

Add routerLink HoC #3431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
11 changes: 11 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [`<Router>`](#router)
- [`<Link>`](#link)
- [`<IndexLink>`](#indexlink)
- [`routerLink`](#routerlinklinkcomponent)
- [`withRouter`](#withroutercomponent)
- [`<RouterContext>`](#routercontext)
- [`context.router`](#contextrouter)
Expand Down Expand Up @@ -163,6 +164,16 @@ Given a route like `<Route path="/users/:userId" />`:
### `<IndexLink>`
An `<IndexLink>` is like a [`<Link>`](#link), except it is only active when the current route is exactly the linked route. It is equivalent to `<Link>` with the `onlyActiveOnIndex` prop set.

### `routerLink(linkComponent)`
A HoC (higher-order component) that wraps another component representing a "link".
It passes the following additional properties to the wrapped component `linkComponent`:

* `linkActive`: a boolean that is `true` if the provided link matches the active route
* `linkHref`: a string representing the "formatted" link
* `handleClick`: a function managing the transition

The properties used by the HoC are `to`, `onClick(e)`, `onlyActiveOnIndex`, `target`, `query` (deprecated), `hash` (deprecated), `state` (deprecated) with the same semantic of [`<Link>`](#link).

### `withRouter(component)`
A HoC (higher-order component) that wraps another component to provide `this.props.router`. Pass in your component and it will return the wrapped component.

Expand Down
105 changes: 21 additions & 84 deletions modules/Link.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import React from 'react'
import warning from './routerWarning'
import { routerShape } from './PropTypes'

const { bool, object, string, func, oneOfType } = React.PropTypes

function isLeftClickEvent(event) {
return event.button === 0
}
import routerLink from './routerLink'

function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
}
const { bool, object, string, func, oneOfType } = React.PropTypes

// TODO: De-duplicate against hasAnyProperties in createTransitionManager.
function isEmptyObject(object) {
Expand All @@ -21,14 +13,6 @@ function isEmptyObject(object) {
return true
}

function createLocationDescriptor(to, { query, hash, state }) {
if (query || hash || state) {
return { pathname: to, query, hash, state }
}

return to
}

/**
* A <Link> is used to create an <a> element that links to a route.
* When that route is active, the link gets the value of its
Expand All @@ -49,20 +33,14 @@ function createLocationDescriptor(to, { query, hash, state }) {
*/
const Link = React.createClass({

contextTypes: {
router: routerShape
},

propTypes: {
to: oneOfType([ string, object ]).isRequired,
query: object,
hash: string,
state: object,
location: oneOfType([ string, object ]).isRequired,
linkHref: string,
linkActive: bool,
activeStyle: object,
activeClassName: string,
onlyActiveOnIndex: bool.isRequired,
onClick: func,
target: string
handleClick: func.isRequired
},

getDefaultProps() {
Expand All @@ -72,70 +50,29 @@ const Link = React.createClass({
}
},

handleClick(event) {
let allowTransition = true

if (this.props.onClick)
this.props.onClick(event)

if (isModifiedEvent(event) || !isLeftClickEvent(event))
return

if (event.defaultPrevented === true)
allowTransition = false

// If target prop is set (e.g. to "_blank") let browser handle link.
/* istanbul ignore if: untestable with Karma */
if (this.props.target) {
if (!allowTransition)
event.preventDefault()

return
}

event.preventDefault()

if (allowTransition) {
const { to, query, hash, state } = this.props
const location = createLocationDescriptor(to, { query, hash, state })

this.context.router.push(location)
}
},

render() {
const { to, query, hash, state, activeClassName, activeStyle, onlyActiveOnIndex, ...props } = this.props
warning(
!(query || hash || state),
'the `query`, `hash`, and `state` props on `<Link>` are deprecated, use `<Link to={{ pathname, query, hash, state }}/>. http://tiny.cc/router-isActivedeprecated'
)

// Ignore if rendered outside the context of router, simplifies unit testing.
const { router } = this.context

if (router) {
const location = createLocationDescriptor(to, { query, hash, state })
props.href = router.createHref(location)

const {
linkActive, linkHref, // from the HoC routerLink
activeClassName, activeStyle, onlyActiveOnIndex,
...props } = this.props
if (linkActive) {
if (activeClassName || (activeStyle != null && !isEmptyObject(activeStyle))) {
if (router.isActive(location, onlyActiveOnIndex)) {
if (activeClassName) {
if (props.className) {
props.className += ` ${activeClassName}`
} else {
props.className = activeClassName
}
if (activeClassName) {
if (props.className) {
props.className += ` ${activeClassName}`
} else {
props.className = activeClassName
}

if (activeStyle)
props.style = { ...props.style, ...activeStyle }
}

if (activeStyle)
props.style = { ...props.style, ...activeStyle }
}
}

return <a {...props} onClick={this.handleClick} />
return <a {...props} href={linkHref} onClick={this.props.handleClick} />
}

})

export default Link
export default routerLink(Link)
67 changes: 67 additions & 0 deletions modules/__tests__/routerLink-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import expect from 'expect'
import React, { Component } from 'react'
import { render } from 'react-dom'
import createHistory from '../createMemoryHistory'
import Router from '../Router'
import Route from '../Route'
import routerLink from '../routerLink'

describe('routerLink', function () {

class _MyLink extends Component {
render() {
return (
<div
data-linkactive={this.props.linkActive}
data-linkHref={this.props.linkHref}
></div>)
}
}

const MyLink = routerLink(_MyLink)

let node
beforeEach(function () {
node = document.createElement('div')
})

it('knows how to make its hrefs', function () {
render((
<Router history={createHistory('/')}>
<Route path="/" component={() => <MyLink to={{
pathname: '/hello/michael',
query: { the: 'query' },
hash: '#the-hash'
}} /> } />
</Router>
), node, function () {
const linkDiv = node.querySelector('div')
expect(linkDiv.getAttribute('data-linkHref')).toEqual('/hello/michael?the=query#the-hash')
})
})

describe('when the route is "/"', function () {
it('a link to "/" should be active', function () {
render((
<Router history={createHistory('/')}>
<Route path="/" component={() => <MyLink to="/" />} />
</Router>
), node, function () {
const linkDiv = node.querySelector('div')
expect(linkDiv.getAttribute('data-linkactive')).toEqual('true')
})
})
it('a link to "/hello" should not be active', function () {
render((
<Router history={createHistory('/')}>
<Route path="/" component={() => <MyLink to="/hello" />} />
</Router>
), node, function () {
const linkDiv = node.querySelector('div')
expect(linkDiv.getAttribute('data-linkactive')).toEqual('false')
})
})
})


})
1 change: 1 addition & 0 deletions modules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export Router from './Router'
export Link from './Link'
export IndexLink from './IndexLink'
export withRouter from './withRouter'
export routerLink from './routerLink'

/* components (configuration) */
export IndexRedirect from './IndexRedirect'
Expand Down
114 changes: 114 additions & 0 deletions modules/routerLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react'
import warning from './routerWarning'

import hoistStatics from 'hoist-non-react-statics'

import { routerShape } from './PropTypes'

const { bool, object, string, func, oneOfType } = React.PropTypes

function isLeftClickEvent(event) {
return event.button === 0
}

function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
}

function createLocationDescriptor(to, { query, hash, state }) {
if (query || hash || state) {
return { pathname: to, query, hash, state }
}

return to
}

function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

export default function routerLink(WrappedComponent) {
const RouterLink = React.createClass({

contextTypes: {
router: routerShape
},

propTypes: {
to: oneOfType([ string, object ]).isRequired,
query: object,
hash: string,
state: object,
onlyActiveOnIndex: bool.isRequired,
onClick: func,
target: string
},

displayName: `routerLink(${getDisplayName(WrappedComponent)})`,
WrappedComponent: WrappedComponent,

getDefaultProps() {
return {
onlyActiveOnIndex: false
}
},

handleClick(event) {
let allowTransition = true

if (this.props.onClick)
this.props.onClick(event)

if (isModifiedEvent(event) || !isLeftClickEvent(event))
return

if (event.defaultPrevented === true)
allowTransition = false

// If target prop is set (e.g. to "_blank") let browser handle link.
/* istanbul ignore if: untestable with Karma */
if (this.props.target) {
if (!allowTransition)
event.preventDefault()

return
}

event.preventDefault()

if (allowTransition) {
const { to, query, hash, state } = this.props
const location = createLocationDescriptor(to, { query, hash, state })

this.context.router.push(location)
}
},

render() {
const { to, query, hash, state, onlyActiveOnIndex, ...props } = this.props
warning(
!(query || hash || state),
'the `query`, `hash`, and `state` props on `<Link>` are deprecated, use `<Link to={{ pathname, query, hash, state }}/>. http://tiny.cc/router-isActivedeprecated'
)

// Ignore if rendered outside the context of router, simplifies unit testing.
const { router } = this.context

props.location = createLocationDescriptor(to, { query, hash, state })
if (router) {
props.linkHref = router.createHref(props.location)
props.linkActive = router.isActive(props.location, onlyActiveOnIndex)
}

return (
<WrappedComponent
{...props}
handleClick={this.handleClick}
/>
)
}

})

return hoistStatics(RouterLink, WrappedComponent)
}