1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Support events emitter for router (#4726)

Fixes #4679 
* Document usage of `events` router property
* Expose `events` in the `router` context object
This commit is contained in:
Jacob Page 2018-07-05 05:41:18 -07:00 committed by Tim Neutkens
parent a1f5f35c2e
commit 498f37e33f
9 changed files with 184 additions and 18 deletions

View file

@ -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']

View file

@ -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

View file

@ -40,9 +40,11 @@ describe('Static Export', () => {
renderViaHTTP(devContext.port, '/dynamic/one')
])
})
afterAll(() => {
stopApp(context.server)
afterAll(async () => {
await Promise.all([
stopApp(context.server),
killApp(devContext.server)
])
})
ssr(context)

View file

@ -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)

View file

@ -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>
)
}
}

View file

@ -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)

View file

@ -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

View file

@ -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()
})
})

View file

@ -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
}