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:
parent
a1f5f35c2e
commit
498f37e33f
|
@ -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']
|
||||
|
||||
|
|
26
readme.md
26
readme.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
52
test/integration/with-router/components/header-nav.js
Normal file
52
test/integration/with-router/components/header-nav.js
Normal 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)
|
25
test/integration/with-router/pages/_app.js
Normal file
25
test/integration/with-router/pages/_app.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
18
test/integration/with-router/pages/a.js
Normal file
18
test/integration/with-router/pages/a.js
Normal 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)
|
13
test/integration/with-router/pages/b.js
Normal file
13
test/integration/with-router/pages/b.js
Normal 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
|
48
test/integration/with-router/test/index.test.js
Normal file
48
test/integration/with-router/test/index.test.js
Normal 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()
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue