mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Add support for URL objects in Link and Router (#1345)
* Add support for URL objects in Link and Router * Fix typo in comment * Fix possible bug if the `href` prop is `null` * Document the usage of URL objects in Link and Router * Update readme.md * Parse URL to get the host & hostname in `isLocal` This should check if the current location and the checked URL have the same `host` or `hostname`. * Format `as` parameter from object to string if required * Format `href` and `as` inside the construct and componentWillReceiveProps * Use `JSON.stringify` to compare objects * Add usage example * chore(package): update chromedriver to version 2.28.0 (#1386) https://greenkeeper.io/ * Refactor the codebase a bit. * Change the example name. * Add a few test cases. * Add the example to the README.
This commit is contained in:
parent
1ae3c2e637
commit
38822717a9
29
examples/with-url-object-routing/README.md
Normal file
29
examples/with-url-object-routing/README.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# URL object routing
|
||||
|
||||
## How to use
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-url-object-routing
|
||||
cd with-url-object-routing
|
||||
```
|
||||
|
||||
Install it and run:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
|
||||
|
||||
```bash
|
||||
now
|
||||
```
|
||||
|
||||
## The idea behind the example
|
||||
|
||||
Next.js allows using [Node.js URL objects](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) as `href` and `as` values for `<Link>` component and parameters of `Router#push` and `Router#replace`.
|
||||
|
||||
This simplify the usage of parameterized URLs when you have many query values.
|
13
examples/with-url-object-routing/package.json
Normal file
13
examples/with-url-object-routing/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "next build",
|
||||
"start": "NODE_ENV=production node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "beta",
|
||||
"path-match": "1.2.4",
|
||||
"react": "^15.4.2",
|
||||
"react-dom": "^15.4.2"
|
||||
}
|
||||
}
|
28
examples/with-url-object-routing/pages/about.js
Normal file
28
examples/with-url-object-routing/pages/about.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Router from 'next/router'
|
||||
|
||||
const href = {
|
||||
pathname: '/about',
|
||||
query: { name: 'zeit' }
|
||||
}
|
||||
|
||||
const as = {
|
||||
pathname: '/about/zeit',
|
||||
hash: 'title-1'
|
||||
}
|
||||
|
||||
const handleClick = () => Router.push(href, as)
|
||||
|
||||
export default (props) => (
|
||||
<div>
|
||||
<h1>About {props.url.query.name}</h1>
|
||||
{props.url.query.name === 'zeit' ? (
|
||||
<Link href='/'>
|
||||
<a>Go to home page</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button onClick={handleClick}>Go to /about/zeit</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
21
examples/with-url-object-routing/pages/index.js
Normal file
21
examples/with-url-object-routing/pages/index.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
const href = {
|
||||
pathname: '/about',
|
||||
query: { name: 'next' }
|
||||
}
|
||||
|
||||
const as = {
|
||||
pathname: '/about/next',
|
||||
hash: 'title-1'
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<h1>Home page</h1>
|
||||
<Link href={href} as={as}>
|
||||
<a>Go to /about/next</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
28
examples/with-url-object-routing/server.js
Normal file
28
examples/with-url-object-routing/server.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const { createServer } = require('http')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
const pathMatch = require('path-match')
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
const route = pathMatch()
|
||||
const match = route('/about/:name')
|
||||
|
||||
app.prepare()
|
||||
.then(() => {
|
||||
createServer((req, res) => {
|
||||
const { pathname } = parse(req.url)
|
||||
const params = match(pathname)
|
||||
if (params === false) {
|
||||
handle(req, res)
|
||||
return
|
||||
}
|
||||
|
||||
app.render(req, res, '/about', params)
|
||||
})
|
||||
.listen(3000, (err) => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on http://localhost:3000')
|
||||
})
|
||||
})
|
38
lib/link.js
38
lib/link.js
|
@ -1,12 +1,13 @@
|
|||
import { resolve } from 'url'
|
||||
import { resolve, format, parse } from 'url'
|
||||
import React, { Component, Children, PropTypes } from 'react'
|
||||
import Router from './router'
|
||||
import { warn, execOnce, getLocationOrigin } from './utils'
|
||||
|
||||
export default class Link extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
constructor (props, ...rest) {
|
||||
super(props, ...rest)
|
||||
this.linkClicked = this.linkClicked.bind(this)
|
||||
this.formatUrls(props)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
@ -25,6 +26,10 @@ export default class Link extends Component {
|
|||
]).isRequired
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.formatUrls(nextProps)
|
||||
}
|
||||
|
||||
linkClicked (e) {
|
||||
if (e.currentTarget.nodeName === 'A' &&
|
||||
(e.metaKey || e.ctrlKey || e.shiftKey || (e.nativeEvent && e.nativeEvent.which === 2))) {
|
||||
|
@ -32,7 +37,7 @@ export default class Link extends Component {
|
|||
return
|
||||
}
|
||||
|
||||
let { href, as } = this.props
|
||||
let { href, as } = this
|
||||
|
||||
if (!isLocal(href)) {
|
||||
// ignore click if it's outside our scope
|
||||
|
@ -68,7 +73,7 @@ export default class Link extends Component {
|
|||
|
||||
// Prefetch the JSON page if asked (only in the client)
|
||||
const { pathname } = window.location
|
||||
const href = resolve(pathname, this.props.href)
|
||||
const href = resolve(pathname, this.href)
|
||||
Router.prefetch(href)
|
||||
}
|
||||
|
||||
|
@ -77,13 +82,25 @@ export default class Link extends Component {
|
|||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.href !== prevProps.href) {
|
||||
if (JSON.stringify(this.props.href) !== JSON.stringify(prevProps.href)) {
|
||||
this.prefetch()
|
||||
}
|
||||
}
|
||||
|
||||
// We accept both 'href' and 'as' as objects which we can pass to `url.format`.
|
||||
// We'll handle it here.
|
||||
formatUrls (props) {
|
||||
this.href = props.href && typeof props.href === 'object'
|
||||
? format(props.href)
|
||||
: props.href
|
||||
this.as = props.as && typeof props.as === 'object'
|
||||
? format(props.as)
|
||||
: props.as
|
||||
}
|
||||
|
||||
render () {
|
||||
let { children } = this.props
|
||||
let { href, as } = this
|
||||
// Deprecated. Warning shown by propType check. If the childen provided is a string (<Link>example</Link>) we wrap it in an <a> tag
|
||||
if (typeof children === 'string') {
|
||||
children = <a>{children}</a>
|
||||
|
@ -97,7 +114,7 @@ export default class Link extends Component {
|
|||
|
||||
// If child is an <a> tag and doesn't have a href attribute we specify it so that repetition is not needed by the user
|
||||
if (child.type === 'a' && !('href' in child.props)) {
|
||||
props.href = this.props.as || this.props.href
|
||||
props.href = as || href
|
||||
}
|
||||
|
||||
return React.cloneElement(child, props)
|
||||
|
@ -105,9 +122,10 @@ export default class Link extends Component {
|
|||
}
|
||||
|
||||
function isLocal (href) {
|
||||
const origin = getLocationOrigin()
|
||||
return !/^(https?:)?\/\//.test(href) ||
|
||||
origin === href.substr(0, origin.length)
|
||||
const url = parse(href, false, true)
|
||||
const origin = parse(getLocationOrigin(), false, true)
|
||||
return (!url.host || !url.hostname) ||
|
||||
(origin.host === url.host || origin.hostname === url.hostname)
|
||||
}
|
||||
|
||||
const warnLink = execOnce(warn)
|
||||
|
|
|
@ -128,7 +128,12 @@ export default class Router extends EventEmitter {
|
|||
return this.change('replaceState', url, as, options)
|
||||
}
|
||||
|
||||
async change (method, url, as, options) {
|
||||
async change (method, _url, _as, options) {
|
||||
// If url and as provided as an object representation,
|
||||
// we'll format them into the string version here.
|
||||
const url = typeof _url === 'object' ? format(_url) : _url
|
||||
const as = typeof _as === 'object' ? format(_as) : _as
|
||||
|
||||
this.abortComponentLoad(as)
|
||||
const { pathname, query } = parse(url, true)
|
||||
|
||||
|
|
39
readme.md
39
readme.md
|
@ -271,6 +271,27 @@ Each top-level component receives a `url` property with the following API:
|
|||
|
||||
The second `as` parameter for `push` and `replace` is an optional _decoration_ of the URL. Useful if you configured custom routes on the server.
|
||||
|
||||
##### With URL object
|
||||
|
||||
<p><details>
|
||||
<summary><b>Examples</b></summary>
|
||||
<ul>
|
||||
<li><a href="./examples/with-url-object-routing">With URL Object Routing</a></li>
|
||||
</ul>
|
||||
</details></p>
|
||||
|
||||
The component `<Link>` can also receive an URL object and it will automatically format it to create the URL string.
|
||||
|
||||
```jsx
|
||||
// pages/index.js
|
||||
import Link from 'next/link'
|
||||
export default () => (
|
||||
<div>Click <Link href={{ pathname: 'about', query: { name: 'Zeit' }}}<a>here</a></Link> to read more</div>
|
||||
)
|
||||
```
|
||||
|
||||
That will generate the URL string `/about?name=Zeit`, you can use every property as defined in the [Node.js URL module documentation](https://nodejs.org/api/url.html#url_url_strings_and_url_objects).
|
||||
|
||||
#### Imperatively
|
||||
|
||||
<p><details>
|
||||
|
@ -303,6 +324,24 @@ The second `as` parameter for `push` and `replace` is an optional _decoration_ o
|
|||
|
||||
_Note: in order to programmatically change the route without triggering navigation and component-fetching, use `props.url.push` and `props.url.replace` within a component_
|
||||
|
||||
##### With URL object
|
||||
You can use an URL object the same way you use it in a `<Link>` component to `push` and `replace` an url.
|
||||
|
||||
```jsx
|
||||
import Router from 'next/router'
|
||||
|
||||
const handler = () => Router.push({
|
||||
pathname: 'about',
|
||||
query: { name: 'Zeit' }
|
||||
})
|
||||
|
||||
export default () => (
|
||||
<div>Click <span onClick={handler}>here</span> to read more</div>
|
||||
)
|
||||
```
|
||||
|
||||
This uses of the same exact parameters as in the `<Link>` component.
|
||||
|
||||
##### Router Events
|
||||
|
||||
You can also listen to different events happening inside the Router.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Link from 'next/link'
|
||||
import { Component } from 'react'
|
||||
import Router from 'next/router'
|
||||
|
||||
let counter = 0
|
||||
|
||||
|
@ -13,6 +14,12 @@ export default class extends Component {
|
|||
this.forceUpdate()
|
||||
}
|
||||
|
||||
visitQueryStringPage () {
|
||||
const href = { pathname: '/nav/querystring', query: { id: 10 } }
|
||||
const as = { pathname: '/nav/querystring/10', hash: '10' }
|
||||
Router.push(href, as)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='nav-home'>
|
||||
|
@ -20,6 +27,20 @@ export default class extends Component {
|
|||
<Link href='/empty-get-initial-props'><a id='empty-props' style={linkStyle}>Empty Props</a></Link>
|
||||
<Link href='/nav/self-reload'><a id='self-reload-link' style={linkStyle}>Self Reload</a></Link>
|
||||
<Link href='/nav/shallow-routing'><a id='shallow-routing-link' style={linkStyle}>Shallow Routing</a></Link>
|
||||
<Link
|
||||
href={{ pathname: '/nav/querystring', query: { id: 10 } }}
|
||||
as={{ pathname: '/nav/querystring/10', hash: '10' }}
|
||||
>
|
||||
<a id='query-string-link' style={linkStyle}>QueryString</a>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => this.visitQueryStringPage()}
|
||||
style={linkStyle}
|
||||
id='query-string-button'
|
||||
>
|
||||
Visit QueryString Page
|
||||
</button>
|
||||
|
||||
<p>This is the home.</p>
|
||||
<div id='counter'>
|
||||
Counter: {counter}
|
||||
|
|
|
@ -8,7 +8,7 @@ export default class AsyncProps extends React.Component {
|
|||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<div className='nav-querystring'>
|
||||
<Link href={`/nav/querystring?id=${parseInt(this.props.id) + 1}`}>
|
||||
<a id='next-id-link'>Click here</a>
|
||||
</Link>
|
||||
|
|
|
@ -236,5 +236,33 @@ export default (context, render) => {
|
|||
browser.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with URL objects', () => {
|
||||
it('should work with <Link/>', async () => {
|
||||
const browser = await webdriver(context.appPort, '/nav')
|
||||
const text = await browser
|
||||
.elementByCss('#query-string-link').click()
|
||||
.waitForElementByCss('.nav-querystring')
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('10')
|
||||
|
||||
expect(await browser.url())
|
||||
.toBe(`http://localhost:${context.appPort}/nav/querystring/10#10`)
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should work with "Router.push"', async () => {
|
||||
const browser = await webdriver(context.appPort, '/nav')
|
||||
const text = await browser
|
||||
.elementByCss('#query-string-button').click()
|
||||
.waitForElementByCss('.nav-querystring')
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('10')
|
||||
|
||||
expect(await browser.url())
|
||||
.toBe(`http://localhost:${context.appPort}/nav/querystring/10#10`)
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue