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

Introduce "asPath" into router and getInitialProps (#1857)

* Add asPath to next/router and getInitialProps context.

* Add test cases.

* Update docs.

* Build as-path pages before they use.
This commit is contained in:
Arunoda Susiripala 2017-05-03 09:40:09 -07:00 committed by GitHub
parent 6e89d49f99
commit 24f3f143a6
12 changed files with 106 additions and 14 deletions

View file

@ -29,6 +29,8 @@ const {
location location
} = window } = window
const asPath = getURL()
const pageLoader = new PageLoader(buildId, assetPrefix) const pageLoader = new PageLoader(buildId, assetPrefix)
window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => { window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => {
pageLoader.registerPage(route, fn) pageLoader.registerPage(route, fn)
@ -56,7 +58,7 @@ export default async () => {
Component = ErrorComponent Component = ErrorComponent
} }
router = createRouter(pathname, query, getURL(), { router = createRouter(pathname, query, asPath, {
pageLoader, pageLoader,
Component, Component,
ErrorComponent, ErrorComponent,
@ -107,7 +109,7 @@ export async function renderError (error) {
console.error(errorMessage) console.error(errorMessage)
if (prod) { if (prod) {
const initProps = { err: error, pathname, query } const initProps = { err: error, pathname, query, asPath }
const props = await loadGetInitialProps(ErrorComponent, initProps) const props = await loadGetInitialProps(ErrorComponent, initProps)
ReactDOM.render(createElement(ErrorComponent, props), errorContainer) ReactDOM.render(createElement(ErrorComponent, props), errorContainer)
} else { } else {
@ -120,8 +122,8 @@ async function doRender ({ Component, props, hash, err, emitter }) {
Component !== ErrorComponent && Component !== ErrorComponent &&
lastAppProps.Component === ErrorComponent) { lastAppProps.Component === ErrorComponent) {
// fetch props if ErrorComponent was replaced with a page component by HMR // fetch props if ErrorComponent was replaced with a page component by HMR
const { pathname, query } = router const { pathname, query, asPath } = router
props = await loadGetInitialProps(Component, { err, pathname, query }) props = await loadGetInitialProps(Component, { err, pathname, query, asPath })
} }
if (emitter) { if (emitter) {

View file

@ -13,7 +13,7 @@ const SingletonRouter = {
} }
// Create public properties and methods of the router in the SingletonRouter // Create public properties and methods of the router in the SingletonRouter
const propertyFields = ['components', 'pathname', 'route', 'query'] const propertyFields = ['components', 'pathname', 'route', 'query', 'asPath']
const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch'] const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch']
const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError'] const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError']

View file

@ -27,7 +27,7 @@ export default class Router {
this.ErrorComponent = ErrorComponent this.ErrorComponent = ErrorComponent
this.pathname = pathname this.pathname = pathname
this.query = query this.query = query
this.as = as this.asPath = as
this.subscriptions = new Set() this.subscriptions = new Set()
this.componentLoadCancel = null this.componentLoadCancel = null
this.onPopState = this.onPopState.bind(this) this.onPopState = this.onPopState.bind(this)
@ -190,7 +190,7 @@ export default class Router {
} }
const { Component } = routeInfo const { Component } = routeInfo
const ctx = { pathname, query } const ctx = { pathname, query, asPath: as }
routeInfo.props = await this.getInitialProps(Component, ctx) routeInfo.props = await this.getInitialProps(Component, ctx)
this.components[route] = routeInfo this.components[route] = routeInfo
@ -229,13 +229,13 @@ export default class Router {
this.route = route this.route = route
this.pathname = pathname this.pathname = pathname
this.query = query this.query = query
this.as = as this.asPath = as
this.notify(data) this.notify(data)
} }
onlyAHashChange (as) { onlyAHashChange (as) {
if (!this.as) return false if (!this.asPath) return false
const [ oldUrlNoHash ] = this.as.split('#') const [ oldUrlNoHash ] = this.asPath.split('#')
const [ newUrlNoHash, newHash ] = as.split('#') const [ newUrlNoHash, newHash ] = as.split('#')
// If the urls are change, there's more than a hash change // If the urls are change, there's more than a hash change

View file

@ -238,6 +238,7 @@ export default Page
- `pathname` - path section of URL - `pathname` - path section of URL
- `query` - query string section of URL parsed as an object - `query` - query string section of URL parsed as an object
- `asPath` - the actual url path
- `req` - HTTP request object (server only) - `req` - HTTP request object (server only)
- `res` - HTTP response object (server only) - `res` - HTTP response object (server only)
- `jsonPageRes` - [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object (client only) - `jsonPageRes` - [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object (client only)
@ -283,6 +284,7 @@ Each top-level component receives a `url` property with the following API:
- `pathname` - `String` of the current path excluding the query string - `pathname` - `String` of the current path excluding the query string
- `query` - `Object` with the parsed query string. Defaults to `{}` - `query` - `Object` with the parsed query string. Defaults to `{}`
- `asPath` - `String` of the actual path (including the query) shows in the browser
- `push(url, as=url)` - performs a `pushState` call with the given url - `push(url, as=url)` - performs a `pushState` call with the given url
- `replace(url, as=url)` - performs a `replaceState` call with the given url - `replace(url, as=url)` - performs a `replaceState` call with the given url

View file

@ -54,7 +54,8 @@ async function doRender (req, res, pathname, query, {
]) ])
Component = Component.default || Component Component = Component.default || Component
Document = Document.default || Document Document = Document.default || Document
const ctx = { err, req, res, pathname, query } const asPath = req.url
const ctx = { err, req, res, pathname, query, asPath }
const props = await loadGetInitialProps(Component, ctx) const props = await loadGetInitialProps(Component, ctx)
// the response might be finshed on the getinitialprops call // the response might be finshed on the getinitialprops call

View file

@ -0,0 +1,22 @@
import React from 'react'
import Router from 'next/router'
export default class extends React.Component {
constructor (...args) {
super(...args)
this.state = {}
}
componentDidMount () {
const asPath = Router.asPath
this.setState({ asPath })
}
render () {
return (
<div className='as-path-content'>
{this.state.asPath}
</div>
)
}
}

View file

@ -0,0 +1,15 @@
import React from 'react'
export default class extends React.Component {
static getInitialProps ({ asPath, req }) {
return { asPath }
}
render () {
return (
<div className='as-path-content'>
{this.props.asPath}
</div>
)
}
}

View file

@ -35,6 +35,9 @@ export default class extends Component {
<a id='query-string-link' style={linkStyle}>QueryString</a> <a id='query-string-link' style={linkStyle}>QueryString</a>
</Link> </Link>
<Link href='/nav/about' replace><a id='about-replace-link' style={linkStyle}>Replace state</a></Link> <Link href='/nav/about' replace><a id='about-replace-link' style={linkStyle}>Replace state</a></Link>
<Link href='/nav/as-path' as='/as/path'><a id='as-path-link' style={linkStyle}>As Path</a></Link>
<Link href='/nav/as-path'><a id='as-path-link-no-as' style={linkStyle}>As Path (No as)</a></Link>
<Link href='/nav/as-path-using-router'><a id='as-path-using-router-link' style={linkStyle}>As Path (Using Router)</a></Link>
<button <button
onClick={() => this.visitQueryStringPage()} onClick={() => this.visitQueryStringPage()}
style={linkStyle} style={linkStyle}

View file

@ -347,5 +347,44 @@ export default (context, render) => {
browser.close() browser.close()
}) })
}) })
describe('with asPath', () => {
describe('inside getInitialProps', () => {
it('should show the correct asPath with a Link with as prop', async () => {
const browser = await webdriver(context.appPort, '/nav/')
const asPath = await browser
.elementByCss('#as-path-link').click()
.waitForElementByCss('.as-path-content')
.elementByCss('.as-path-content').text()
expect(asPath).toBe('/as/path')
browser.close()
})
it('should show the correct asPath with a Link without the as prop', async () => {
const browser = await webdriver(context.appPort, '/nav/')
const asPath = await browser
.elementByCss('#as-path-link-no-as').click()
.waitForElementByCss('.as-path-content')
.elementByCss('.as-path-content').text()
expect(asPath).toBe('/nav/as-path')
browser.close()
})
})
describe('with next/router', () => {
it('should show the correct asPath', async () => {
const browser = await webdriver(context.appPort, '/nav/')
const asPath = await browser
.elementByCss('#as-path-using-router-link').click()
.waitForElementByCss('.as-path-content')
.elementByCss('.as-path-content').text()
expect(asPath).toBe('/nav/as-path-using-router')
browser.close()
})
})
})
}) })
} }

View file

@ -51,6 +51,8 @@ describe('Basic Features', () => {
renderViaHTTP(context.appPort, '/nav/hash-changes'), renderViaHTTP(context.appPort, '/nav/hash-changes'),
renderViaHTTP(context.appPort, '/nav/shallow-routing'), renderViaHTTP(context.appPort, '/nav/shallow-routing'),
renderViaHTTP(context.appPort, '/nav/redirect'), renderViaHTTP(context.appPort, '/nav/redirect'),
renderViaHTTP(context.appPort, '/nav/as-path'),
renderViaHTTP(context.appPort, '/nav/as-path-using-router'),
renderViaHTTP(context.appPort, '/nested-cdm/index') renderViaHTTP(context.appPort, '/nested-cdm/index')
]) ])

View file

@ -3,8 +3,8 @@
import cheerio from 'cheerio' import cheerio from 'cheerio'
export default function ({ app }, suiteName, render) { export default function ({ app }, suiteName, render) {
async function get$ (path) { async function get$ (path, query) {
const html = await render(path) const html = await render(path, query)
return cheerio.load(html) return cheerio.load(html)
} }
@ -69,6 +69,11 @@ export default function ({ app }, suiteName, render) {
expect($('pre').text()).toMatch(/This is an expected error/) expect($('pre').text()).toMatch(/This is an expected error/)
}) })
test('asPath', async () => {
const $ = await get$('/nav/as-path', { aa: 10 })
expect($('.as-path-content').text()).toBe('/nav/as-path?aa=10')
})
test('error 404', async () => { test('error 404', async () => {
const $ = await get$('/non-existent') const $ = await get$('/non-existent')
expect($('h1').text()).toBe('404') expect($('h1').text()).toBe('404')

View file

@ -11,7 +11,8 @@ export const nextBuild = build
export const pkg = _pkg export const pkg = _pkg
export function renderViaAPI (app, pathname, query = {}) { export function renderViaAPI (app, pathname, query = {}) {
return app.renderToHTML({}, {}, pathname, query) const url = `${pathname}?${qs.stringify(query)}`
return app.renderToHTML({ url }, {}, pathname, query)
} }
export function renderViaHTTP (appPort, pathname, query = {}) { export function renderViaHTTP (appPort, pathname, query = {}) {