diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 587f60f02..138f4bbe3 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -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) diff --git a/docs/en/api/options.md b/docs/en/api/options.md index b72f2ad50..5de3e690a 100644 --- a/docs/en/api/options.md +++ b/docs/en/api/options.md @@ -13,6 +13,7 @@ name?: string; // for named routes components?: { [name: string]: Component }; // for named views redirect?: string | Location | Function; + props?: boolean | string | Function; alias?: string | Array; children?: Array; // for nested routes beforeEnter?: (to: Route, from: Route, next: Function) => void; diff --git a/docs/en/essentials/passing-props.md b/docs/en/essentials/passing-props.md new file mode 100644 index 000000000..b6e20bdf2 --- /dev/null +++ b/docs/en/essentials/passing-props.md @@ -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: '
User {{ $route.params.id }}
' +} +const router = new VueRouter({ + routes: [ + { path: '/user/:id', component: User } + ] +}) +``` + +**👍 Decoupled with props** + +``` js +const User = { + props: ['id'], + template: '
User {{ id }}
' +} +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 route.params will be set as the component props. + +### Object mode + +When props is an object, this will be set as the component props as-is. +Useful for when the props are static. + +``` js +const router = new VueRouter({ + routes: [ + { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } } + ] +}) +``` + +### Function mode + +You can create a function that returns props. +This allows you to to cast the parameter to another type, combine static values with route-based values, etc. + +``` 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. +Use a wrapper component if you need state to define the props, that way vue can react to state changes. + + +For advanced usage, checkout the [example](https://github.com/vuejs/vue-router/blob/dev/examples/route-props/app.js). diff --git a/examples/index.html b/examples/index.html index 358d5f3fa..afde345b1 100644 --- a/examples/index.html +++ b/examples/index.html @@ -15,6 +15,7 @@

Vue Router Examples

  • Route Matching
  • Active Links
  • Redirect
  • +
  • Route Props
  • Route Alias
  • Transitions
  • Data Fetching
  • diff --git a/examples/route-props/Hello.vue b/examples/route-props/Hello.vue new file mode 100644 index 000000000..1205c6da5 --- /dev/null +++ b/examples/route-props/Hello.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/examples/route-props/app.js b/examples/route-props/app.js new file mode 100644 index 000000000..d39e682b3 --- /dev/null +++ b/examples/route-props/app.js @@ -0,0 +1,39 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' +import Hello from './Hello.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 }, // Pass route.params to props + { path: '/static', component: Hello, props: { name: 'world' }}, // static values + { path: '/dynamic/:years', component: Hello, props: dynamicPropsFn } // custom logic for mapping between route and props + ] +}) + +new Vue({ + router, + template: ` +
    +

    Route props

    +
      +
    • /
    • +
    • /hello/you
    • +
    • /static
    • +
    • /dynamic/1
    • +
    + +
    + ` +}).$mount('#app') diff --git a/examples/route-props/index.html b/examples/route-props/index.html new file mode 100644 index 000000000..0ebff0640 --- /dev/null +++ b/examples/route-props/index.html @@ -0,0 +1,6 @@ + + +← Examples index +
    + + diff --git a/src/components/view.js b/src/components/view.js index c2d0e4c82..5cb8e9e67 100644 --- a/src/components/view.js +++ b/src/components/view.js @@ -1,3 +1,5 @@ +import { resolveProps } from '../util/props' + export default { name: 'router-view', functional: true, @@ -56,6 +58,7 @@ export default { matched.instances[name] = undefined } } + data.props = resolveProps(route, component, matched.props && matched.props[name]) return h(component, data, children) } diff --git a/src/create-route-map.js b/src/create-route-map.js index c04ddd497..e00448ed5 100644 --- a/src/create-route-map.js +++ b/src/create-route-map.js @@ -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 || {} diff --git a/src/util/props.js b/src/util/props.js new file mode 100644 index 000000000..c8889f2ba --- /dev/null +++ b/src/util/props.js @@ -0,0 +1,26 @@ + +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 + } + return route.params + + default: + warn(false, `props in "${route.path}" is a ${typeof config}, expecting an object, function or boolean.`) + } +} + diff --git a/test/e2e/nightwatch.config.js b/test/e2e/nightwatch.config.js index f3dd11859..08daf7ab8 100644 --- a/test/e2e/nightwatch.config.js +++ b/test/e2e/nightwatch.config.js @@ -1,4 +1,5 @@ // http://nightwatchjs.org/guide#settings-file + module.exports = { 'src_folders': ['test/e2e/specs'], 'output_folder': 'test/e2e/reports', @@ -7,7 +8,7 @@ module.exports = { 'selenium': { 'start_process': true, - 'server_path': 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar', + 'server_path': require('selenium-server').path, 'host': '127.0.0.1', 'port': 4444, 'cli_args': { diff --git a/test/e2e/specs/route-props.js b/test/e2e/specs/route-props.js new file mode 100644 index 000000000..7327be96e --- /dev/null +++ b/test/e2e/specs/route-props.js @@ -0,0 +1,25 @@ +module.exports = { + 'route-props': function (browser) { + browser + .url('http://localhost:8080/route-props/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 4) + + .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)+ '!') + + .end() + } +}