Skip to content

New feature: Passing props to components from vue-router #973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 19, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Named Routes](essentials/named-routes.md)
- [Named Views](essentials/named-views.md)
- [Redirect and Alias](essentials/redirect-and-alias.md)
- [Passing props](essentials/passing-props.md)
- [HTML5 History Mode](essentials/history-mode.md)
- Advanced
- [Navigation Guards](advanced/navigation-guards.md)
Expand Down
1 change: 1 addition & 0 deletions docs/en/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
name?: string; // for named routes
components?: { [name: string]: Component }; // for named views
redirect?: string | Location | Function;
props?: boolean | string | Function;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is Boolean | Object | Function in passing-props.md

alias?: string | Array<string>;
children?: Array<RouteConfig>; // for nested routes
beforeEnter?: (to: Route, from: Route, next: Function) => void;
Expand Down
72 changes: 72 additions & 0 deletions docs/en/essentials/passing-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Passing props

Using `$route` in your component creates a tight coupling with the route which limits the flexibility of the component as it can only be used on certain urls.

To decouple this component from the router use props:

**❌ Coupled to $route**

``` js
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})
```

**👍 Decoupled with props**

``` js
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true }
]
})
```

This allows you to use the component anywhere, which makes the component easier to reuse and test.

### Boolean mode

When props is set to true, the router looks at the component and searches for props with the same name as the route.params.
When a prop is found and its type is Number, String or not set, the param is passed as prop.

### Object mode

When props is an object, this will be set as the component props as-is.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be even better to say when it's useful to pass an object as the props

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've improved the description and code snippet.


``` js
const router = new VueRouter({
routes: [
{ path: '/user/first-one', component: User, props: {id: '1' } }
]
})
```

### Function mode

You can create a function that returns props.
This allows you to mix static values and values based on the route.

``` js
const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})
```

The url: `/search?q=vue` would pass `{query: "vue"}` as props to the SearchUser component.

Try to keep the props function stateless, as it's only evaluated on route changes.
Tip: If you need to set props based on route and state create a wrapper component.


For advanced usage, checkout the [example](https://github.com/vuejs/vue-router/blob/dev/examples/route-props/app.js).
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>Vue Router Examples</h1>
<li><a href="route-matching">Route Matching</a></li>
<li><a href="active-links">Active Links</a></li>
<li><a href="redirect">Redirect</a></li>
<li><a href="route-props">Route Props</a></li>
<li><a href="route-alias">Route Alias</a></li>
<li><a href="transitions">Transitions</a></li>
<li><a href="data-fetching">Data Fetching</a></li>
Expand Down
22 changes: 22 additions & 0 deletions examples/route-props/Calculator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div>
<h2 class="calculator_add">{{a}} + {{b}} = {{ a + b }}</h2>
<h2 class="calculator_multiply">{{a}} * {{b}} = {{ a * b }}</h2>
</div>
</template>

<script>

export default {
props: {
a: {
type: Number,
required: true
},
b: {
type: Number,
required: true
}
}
}
</script>
17 changes: 17 additions & 0 deletions examples/route-props/Hello.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div>
<h2 class="hello">Hello {{name}}</h2>
</div>
</template>

<script>

export default {
props: {
name: {
type: String,
default: 'Vue!'
}
}
}
</script>
42 changes: 42 additions & 0 deletions examples/route-props/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Hello from './Hello.vue'
import Calculator from './Calculator.vue'

Vue.use(VueRouter)

function dynamicPropsFn (route) {
const now = new Date()
return {
name: (now.getFullYear() + parseInt(route.params.years)) + '!'
}
}

const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/', component: Hello }, // No props, no nothing
{ path: '/hello/:name', component: Hello, props: true }, // Match params to props (Only matches props with type: String, Number or untyped)
{ path: '/static', component: Hello, props: { name: 'world' }}, // static values
{ path: '/dynamic/:years', component: Hello, props: dynamicPropsFn }, // custom logic for mapping between route and props
{ path: '/calculator/:a/:b', component: Calculator, props: true } // Props with type Number are passed as number
]
})

new Vue({
router,
template: `
<div id="app">
<h1>Route props</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/hello/you">/hello/you</router-link></li>
<li><router-link to="/static">/static</router-link></li>
<li><router-link to="/dynamic/1">/dynamic/1</router-link></li>
<li><router-link to="/calculator/2/3">/calculator/2/3</router-link></li>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')
6 changes: 6 additions & 0 deletions examples/route-props/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<script src="/__build__/shared.js"></script>
<script src="/__build__/route-props.js"></script>
3 changes: 3 additions & 0 deletions src/components/view.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { resolveProps } from '../util/props'

export default {
name: 'router-view',
functional: true,
Expand Down Expand Up @@ -49,6 +51,7 @@ export default {
matched.instances[name] = undefined
}
}
data.props = resolveProps(route, component, matched.props && matched.props[name])
Copy link
Member

@fnlctrl fnlctrl Dec 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should merge with the original data.props, or it would be a breaking change as the current code disallows explicitly passing props to <router-view>, like <router-view foo="bar"/>

Edit: Sorry, I was wrong about that..

Copy link
Contributor Author

@bfanger bfanger Dec 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props of <router-view> are still passed to the rendered element. The <div> in Hello still gets the class="view"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some further testing and setting properties on <router-view> overwrites the values set in data.props, so they are merged somewhere

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's normal Vue behaviour for any component.

}

return h(component, data, children)
Expand Down
1 change: 1 addition & 0 deletions src/create-route-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function addRouteRecord (
name,
parent,
matchAs,
props: typeof route.props === 'undefined' ? {} : (route.components ? route.props : { default: route.props }),
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {}
Expand Down
46 changes: 46 additions & 0 deletions src/util/props.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import { warn } from './warn'

export function resolveProps (route, component, config) {
switch (typeof config) {

case 'undefined':
return

case 'object':
return config

case 'function':
return config(route)

case 'boolean':
if (!config) {
return
}
if (!component.props) {
return
}
const props = {}
for (const prop in route.params) {
const value = route.params[prop]
if (hasProp(component, prop)) {
const propType = component.props[prop].type
Copy link
Member

@fnlctrl fnlctrl Dec 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about reading prop types at runtime and converting them to Number if possible.. Maybe we should just leave them as string since url params are string by nature. And if Number was supported, should we also support Boolean?

Also, if this type conversion were to be supported, type can be an array, so we'll have to loop over that should it be the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not ideal, but the alternative was either not to support Number type, or pass it as an string and get a propType warnings and subtle bugs in itemsById.find(item => item.id === id)

I think routing to ID is a common use-case to allow it in the {props: true} scenario

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in that case, the user should provide a function and convert types by himself. This way we avoid the implicit behavior.

Copy link
Contributor Author

@bfanger bfanger Dec 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified the behavior of the {props: true} which now sets the props to route.params.
Vue complaining about type is much better than silently ignoring some properties and leaving users confused why it doesn't work.

if (propType === null || propType === String) {
props[prop] = value
} else if (propType === Number && value.match(/^-?([0-9]+|[0-9]*\.[0-9]+)$/)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably cache this regex literal /^-?([0-9]+|[0-9]*\.[0-9]+)$/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done

props[prop] = parseFloat(value)
}
}
}
return props
default:
warn(false, `props in "${route.path}" is a ${typeof config}, expecting an object, function or boolean.`)
}
}

function hasProp (component, prop) {
if (!component.props) {
Copy link
Member

@fnlctrl fnlctrl Dec 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably skip this if (!component.props) check since it's already done in https://github.com/bfanger/vue-router/blob/5ad9d480eeed6700a26597484d0c0454d30a13e3/src/util/props.js#L20-L22
Or maybe just inline this function and use the typof expression..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, i've inlined the hasProp function

return false
}
return typeof component.props[prop] !== 'undefined'
}
7 changes: 5 additions & 2 deletions test/e2e/nightwatch.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// http://nightwatchjs.org/guide#settings-file
var seleniumServer = require('selenium-server');
var chromedriver = require('chromedriver');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lines are removed


module.exports = {
'src_folders': ['test/e2e/specs'],
'output_folder': 'test/e2e/reports',
Expand All @@ -7,11 +10,11 @@ module.exports = {

'selenium': {
'start_process': true,
'server_path': 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar',
'server_path': seleniumServer.path,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent we should do server_path': require('selenium-server').path instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

'host': '127.0.0.1',
'port': 4444,
'cli_args': {
'webdriver.chrome.driver': 'node_modules/chromedriver/lib/chromedriver/chromedriver'
'webdriver.chrome.driver': chromedriver.path
}
},

Expand Down
29 changes: 29 additions & 0 deletions test/e2e/specs/route-props.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
'route-props': function (browser) {
browser
.url('http://localhost:8080/route-props/')
.waitForElementVisible('#app', 1000)
.assert.count('li a', 5)

.assert.urlEquals('http://localhost:8080/route-props/')
.assert.containsText('.hello', 'Hello Vue!')

.click('li:nth-child(2) a')
.assert.urlEquals('http://localhost:8080/route-props/hello/you')
.assert.containsText('.hello', 'Hello you')

.click('li:nth-child(3) a')
.assert.urlEquals('http://localhost:8080/route-props/static')
.assert.containsText('.hello', 'Hello world')

.click('li:nth-child(4) a')
.assert.urlEquals('http://localhost:8080/route-props/dynamic/1')
.assert.containsText('.hello', 'Hello ' + ((new Date()).getFullYear() + 1)+ '!')

.click('li:nth-child(5) a')
.assert.urlEquals('http://localhost:8080/route-props/calculator/2/3')
.assert.containsText('.calculator_add', '2 + 3 = 5')
.assert.containsText('.calculator_multiply', '2 * 3 = 6')
.end()
}
}