Skip to content

Commit

Permalink
Support events emitter for router (#4726)
Browse files Browse the repository at this point in the history
Fixes #4679 
* Document usage of `events` router property
* Expose `events` in the `router` context object
  • Loading branch information
jpage-godaddy authored and timneutkens committed Jul 5, 2018
1 parent a1f5f35 commit 498f37e
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 18 deletions.
2 changes: 1 addition & 1 deletion lib/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const SingletonRouter = {

// Create public properties and methods of the router in the SingletonRouter
const urlPropertyFields = ['pathname', 'route', 'query', 'asPath']
const propertyFields = ['components']
const propertyFields = ['components', 'events']
const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError', 'hashChangeStart', 'hashChangeComplete']
const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch', 'beforePopState']

Expand Down
26 changes: 14 additions & 12 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,37 +544,39 @@ This uses the same exact parameters as in the `<Link>` component.
You can also listen to different events happening inside the Router.
Here's a list of supported events:

- `onRouteChangeStart(url)` - Fires when a route starts to change
- `onRouteChangeComplete(url)` - Fires when a route changed completely
- `onRouteChangeError(err, url)` - Fires when there's an error when changing routes
- `onBeforeHistoryChange(url)` - Fires just before changing the browser's history
- `onHashChangeStart(url)` - Fires when the hash will change but not the page
- `onHashChangeComplete(url)` - Fires when the hash has changed but not the page
- `routeChangeStart(url)` - Fires when a route starts to change
- `routeChangeComplete(url)` - Fires when a route changed completely
- `routeChangeError(err, url)` - Fires when there's an error when changing routes
- `beforeHistoryChange(url)` - Fires just before changing the browser's history
- `hashChangeStart(url)` - Fires when the hash will change but not the page
- `hashChangeComplete(url)` - Fires when the hash has changed but not the page

> Here `url` is the URL shown in the browser. If you call `Router.push(url, as)` (or similar), then the value of `url` will be `as`.
Here's how to properly listen to the router event `onRouteChangeStart`:
Here's how to properly listen to the router event `routeChangeStart`:

```js
Router.onRouteChangeStart = url => {
const handleRouteChange = url => {
console.log('App is changing to: ', url)
}

Router.events.on('routeChangeStart', handleRouteChange)
```

If you no longer want to listen to that event, you can simply unset the event listener like this:
If you no longer want to listen to that event, you can unsubscribe with the `off` method:

```js
Router.onRouteChangeStart = null
Router.events.off('routeChangeStart', handleRouteChange)
```

If a route load is cancelled (for example by clicking two links rapidly in succession), `routeChangeError` will fire. The passed `err` will contain a `cancelled` property set to `true`.

```js
Router.onRouteChangeError = (err, url) => {
Router.events.on('routeChangeError', (err, url) => {
if (err.cancelled) {
console.log(`Route to ${url} was cancelled!`)
}
}
})
```

##### Shallow Routing
Expand Down
8 changes: 5 additions & 3 deletions test/integration/static/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ describe('Static Export', () => {
renderViaHTTP(devContext.port, '/dynamic/one')
])
})
afterAll(() => {
stopApp(context.server)
killApp(devContext.server)
afterAll(async () => {
await Promise.all([
stopApp(context.server),
killApp(devContext.server)
])
})

ssr(context)
Expand Down
52 changes: 52 additions & 0 deletions test/integration/with-router/components/header-nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react'
import { withRouter } from 'next/router'
import Link from 'next/link'

const pages = {
'/a': 'Foo',
'/b': 'Bar'
}

class HeaderNav extends React.Component {
constructor ({ router }) {
super()

this.state = {
activeURL: router.asPath
}

this.handleRouteChange = this.handleRouteChange.bind(this)
}

componentDidMount () {
this.props.router.events.on('routeChangeComplete', this.handleRouteChange)
}

componentWillUnmount () {
this.props.router.events.off('routeChangeComplete', this.handleRouteChange)
}

handleRouteChange (url) {
this.setState({
activeURL: url
})
}

render () {
return (
<nav>
{
Object.keys(pages).map(url => (
<Link href={url} key={url} prefetch>
<a className={this.state.activeURL === url ? 'active' : ''}>
{ pages[url] }
</a>
</Link>
))
}
</nav>
)
}
}

export default withRouter(HeaderNav)
25 changes: 25 additions & 0 deletions test/integration/with-router/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import App, { Container } from 'next/app'
import React from 'react'
import HeaderNav from '../components/header-nav'

export default class MyApp extends App {
static async getInitialProps ({ Component, router, ctx }) {
let pageProps = {}

if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}

return {pageProps}
}

render () {
const { Component, pageProps } = this.props
return (
<Container>
<HeaderNav />
<Component {...pageProps} />
</Container>
)
}
}
18 changes: 18 additions & 0 deletions test/integration/with-router/pages/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react'
import { withRouter } from 'next/router'

class PageA extends React.Component {
goToB () {
this.props.router.push('/b')
}

render () {
return (
<div id='page-a'>
<button onClick={() => this.goToB()}>Go to B</button>
</div>
)
}
}

export default withRouter(PageA)
13 changes: 13 additions & 0 deletions test/integration/with-router/pages/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react'

class PageB extends React.Component {
render () {
return (
<div id='page-b'>
<p>Page B!</p>
</div>
)
}
}

export default PageB
48 changes: 48 additions & 0 deletions test/integration/with-router/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* global jasmine, describe, it, expect, beforeAll, afterAll */

import { join } from 'path'
import {
nextServer,
nextBuild,
startApp,
stopApp
} from 'next-test-utils'
import webdriver from 'next-webdriver'

describe('withRouter', () => {
const appDir = join(__dirname, '../')
let appPort
let server
let app
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5

beforeAll(async () => {
await nextBuild(appDir)
app = nextServer({
dir: join(__dirname, '../'),
dev: false,
quiet: true
})

server = await startApp(app)
appPort = server.address().port
})

afterAll(() => stopApp(server))

it('allows observation of navigation events', async () => {
const browser = await webdriver(appPort, '/a')
await browser.waitForElementByCss('#page-a')

let activePage = await browser.elementByCss('.active').text()
expect(activePage).toBe('Foo')

await browser.elementByCss('button').click()
await browser.waitForElementByCss('#page-b')

activePage = await browser.elementByCss('.active').text()
expect(activePage).toBe('Bar')

browser.close()
})
})
10 changes: 8 additions & 2 deletions test/lib/next-webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ function getBrowser (url, timeout) {
reject(error)
}, timeout)

browser.init({browserName: 'chrome'}).get(url, (err) => {
browser.init({browserName: 'chrome'}).get(url, err => {
if (timeouted) {
browser.close()
try {
browser.close(() => {
// Ignore errors
})
} catch (err) {
// Ignore
}
return
}

Expand Down

0 comments on commit 498f37e

Please sign in to comment.