From 498f37e33fbca118adc8e608dc98c46e152e7432 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Thu, 5 Jul 2018 05:41:18 -0700 Subject: [PATCH] Support `events` emitter for router (#4726) Fixes #4679 * Document usage of `events` router property * Expose `events` in the `router` context object --- lib/router/index.js | 2 +- readme.md | 26 +++++----- test/integration/static/test/index.test.js | 8 +-- .../with-router/components/header-nav.js | 52 +++++++++++++++++++ test/integration/with-router/pages/_app.js | 25 +++++++++ test/integration/with-router/pages/a.js | 18 +++++++ test/integration/with-router/pages/b.js | 13 +++++ .../with-router/test/index.test.js | 48 +++++++++++++++++ test/lib/next-webdriver.js | 10 +++- 9 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 test/integration/with-router/components/header-nav.js create mode 100644 test/integration/with-router/pages/_app.js create mode 100644 test/integration/with-router/pages/a.js create mode 100644 test/integration/with-router/pages/b.js create mode 100644 test/integration/with-router/test/index.test.js diff --git a/lib/router/index.js b/lib/router/index.js index 5507e8d6e5622..e9c31c5ecdedc 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -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'] diff --git a/readme.md b/readme.md index 6ff3d58c4eb45..6f65079b39f83 100644 --- a/readme.md +++ b/readme.md @@ -544,37 +544,39 @@ This uses the same exact parameters as in the `` 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 diff --git a/test/integration/static/test/index.test.js b/test/integration/static/test/index.test.js index 3fea134be7d10..753873880a0d9 100644 --- a/test/integration/static/test/index.test.js +++ b/test/integration/static/test/index.test.js @@ -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) diff --git a/test/integration/with-router/components/header-nav.js b/test/integration/with-router/components/header-nav.js new file mode 100644 index 0000000000000..d144033647ce1 --- /dev/null +++ b/test/integration/with-router/components/header-nav.js @@ -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 ( + + ) + } +} + +export default withRouter(HeaderNav) diff --git a/test/integration/with-router/pages/_app.js b/test/integration/with-router/pages/_app.js new file mode 100644 index 0000000000000..a8e4213b489ff --- /dev/null +++ b/test/integration/with-router/pages/_app.js @@ -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 ( + + + + + ) + } +} diff --git a/test/integration/with-router/pages/a.js b/test/integration/with-router/pages/a.js new file mode 100644 index 0000000000000..91bfaaa73712a --- /dev/null +++ b/test/integration/with-router/pages/a.js @@ -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 ( +
+ +
+ ) + } +} + +export default withRouter(PageA) diff --git a/test/integration/with-router/pages/b.js b/test/integration/with-router/pages/b.js new file mode 100644 index 0000000000000..ab927c56313e1 --- /dev/null +++ b/test/integration/with-router/pages/b.js @@ -0,0 +1,13 @@ +import * as React from 'react' + +class PageB extends React.Component { + render () { + return ( +
+

Page B!

+
+ ) + } +} + +export default PageB diff --git a/test/integration/with-router/test/index.test.js b/test/integration/with-router/test/index.test.js new file mode 100644 index 0000000000000..03f2859b4fca3 --- /dev/null +++ b/test/integration/with-router/test/index.test.js @@ -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() + }) +}) diff --git a/test/lib/next-webdriver.js b/test/lib/next-webdriver.js index 55c2ccd2b18bf..4b2d6c59de95d 100644 --- a/test/lib/next-webdriver.js +++ b/test/lib/next-webdriver.js @@ -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 }