From be7a13891833a0b30979a1103c198926a2a533ce Mon Sep 17 00:00:00 2001 From: Bob Fanger Date: Sun, 27 Nov 2016 15:09:55 +0100 Subject: [PATCH 1/6] Let the selenium-server and chromedriver packages determine the paths Makes it easier to upgrade to those packages. --- test/e2e/nightwatch.config.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/nightwatch.config.js b/test/e2e/nightwatch.config.js index 7e48b076b..2db046e57 100644 --- a/test/e2e/nightwatch.config.js +++ b/test/e2e/nightwatch.config.js @@ -1,4 +1,7 @@ // http://nightwatchjs.org/guide#settings-file +var seleniumServer = require('selenium-server'); +var chromedriver = require('chromedriver'); + module.exports = { 'src_folders': ['test/e2e/specs'], 'output_folder': 'test/e2e/reports', @@ -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, 'host': '127.0.0.1', 'port': 4444, 'cli_args': { - 'webdriver.chrome.driver': 'node_modules/chromedriver/lib/chromedriver/chromedriver' + 'webdriver.chrome.driver': chromedriver.path } }, From 5ad9d480eeed6700a26597484d0c0454d30a13e3 Mon Sep 17 00:00:00 2001 From: Bob Fanger Date: Sun, 27 Nov 2016 23:48:04 +0100 Subject: [PATCH 2/6] Feature: routeConfig.props - Passing props to a component --- docs/en/SUMMARY.md | 1 + docs/en/api/options.md | 1 + docs/en/essentials/passing-props.md | 72 +++++++++++++++++++++++++++++ examples/index.html | 1 + examples/route-props/Calculator.vue | 22 +++++++++ examples/route-props/Hello.vue | 17 +++++++ examples/route-props/app.js | 42 +++++++++++++++++ examples/route-props/index.html | 6 +++ src/components/view.js | 3 ++ src/create-route-map.js | 1 + src/util/props.js | 46 ++++++++++++++++++ test/e2e/specs/route-props.js | 29 ++++++++++++ 12 files changed, 241 insertions(+) create mode 100644 docs/en/essentials/passing-props.md create mode 100644 examples/route-props/Calculator.vue create mode 100644 examples/route-props/Hello.vue create mode 100644 examples/route-props/app.js create mode 100644 examples/route-props/index.html create mode 100644 src/util/props.js create mode 100644 test/e2e/specs/route-props.js diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index d88d82b23..e2618d0eb 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..c57f9f25e --- /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 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. + +``` 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). 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/Calculator.vue b/examples/route-props/Calculator.vue new file mode 100644 index 000000000..3b696d945 --- /dev/null +++ b/examples/route-props/Calculator.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file 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..c2d2cfdd1 --- /dev/null +++ b/examples/route-props/app.js @@ -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: ` +
    +

    Route props

    +
      +
    • /
    • +
    • /hello/you
    • +
    • /static
    • +
    • /dynamic/1
    • +
    • /calculator/2/3
    • +
    + +
    + ` +}).$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 14ba99172..29fedb64f 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, @@ -49,6 +51,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..c731f4751 --- /dev/null +++ b/src/util/props.js @@ -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 + if (propType === null || propType === String) { + props[prop] = value + } else if (propType === Number && value.match(/^-?([0-9]+|[0-9]*\.[0-9]+)$/)) { + 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) { + return false + } + return typeof component.props[prop] !== 'undefined' +} diff --git a/test/e2e/specs/route-props.js b/test/e2e/specs/route-props.js new file mode 100644 index 000000000..9461dff22 --- /dev/null +++ b/test/e2e/specs/route-props.js @@ -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() + } +} From 0902926bb05332085646b60f6f3842d5cdcfdaf8 Mon Sep 17 00:00:00 2001 From: Bob Fanger Date: Fri, 2 Dec 2016 14:49:30 +0100 Subject: [PATCH 3/6] inlined hasProp + cached the regex literal --- src/util/props.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/util/props.js b/src/util/props.js index c731f4751..3730224b0 100644 --- a/src/util/props.js +++ b/src/util/props.js @@ -1,6 +1,8 @@ import { warn } from './warn' +const numberRegex = /^-?([0-9]+|[0-9]*\.[0-9]+)$/ + export function resolveProps (route, component, config) { switch (typeof config) { @@ -23,24 +25,19 @@ export function resolveProps (route, component, config) { const props = {} for (const prop in route.params) { const value = route.params[prop] - if (hasProp(component, prop)) { + if (typeof component.props[prop] !== 'undefined') { const propType = component.props[prop].type if (propType === null || propType === String) { props[prop] = value - } else if (propType === Number && value.match(/^-?([0-9]+|[0-9]*\.[0-9]+)$/)) { + } else if (propType === Number && value.match(numberRegex)) { 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) { - return false - } - return typeof component.props[prop] !== 'undefined' -} From 608f6e3232c5de382532b386cb04fc1cd9fe1596 Mon Sep 17 00:00:00 2001 From: Bob Fanger Date: Fri, 2 Dec 2016 15:22:58 +0100 Subject: [PATCH 4/6] Simplied the { props:true } behaviour which is now very predicable. --- docs/en/essentials/passing-props.md | 3 +-- examples/route-props/Calculator.vue | 22 ---------------------- examples/route-props/app.js | 5 +---- src/util/props.js | 19 +------------------ test/e2e/specs/route-props.js | 6 +----- 5 files changed, 4 insertions(+), 51 deletions(-) delete mode 100644 examples/route-props/Calculator.vue diff --git a/docs/en/essentials/passing-props.md b/docs/en/essentials/passing-props.md index c57f9f25e..e0af0b375 100644 --- a/docs/en/essentials/passing-props.md +++ b/docs/en/essentials/passing-props.md @@ -35,8 +35,7 @@ This allows you to use the component anywhere, which makes the component easier ### 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. +When props is set to true, the route.params will be set as the component props. ### Object mode diff --git a/examples/route-props/Calculator.vue b/examples/route-props/Calculator.vue deleted file mode 100644 index 3b696d945..000000000 --- a/examples/route-props/Calculator.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - \ No newline at end of file diff --git a/examples/route-props/app.js b/examples/route-props/app.js index c2d2cfdd1..906fa1de6 100644 --- a/examples/route-props/app.js +++ b/examples/route-props/app.js @@ -1,7 +1,6 @@ import Vue from 'vue' import VueRouter from 'vue-router' import Hello from './Hello.vue' -import Calculator from './Calculator.vue' Vue.use(VueRouter) @@ -19,8 +18,7 @@ const router = new VueRouter({ { 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 + { path: '/dynamic/:years', component: Hello, props: dynamicPropsFn } // custom logic for mapping between route and props ] }) @@ -34,7 +32,6 @@ new Vue({
  • /hello/you
  • /static
  • /dynamic/1
  • -
  • /calculator/2/3
  • diff --git a/src/util/props.js b/src/util/props.js index 3730224b0..c8889f2ba 100644 --- a/src/util/props.js +++ b/src/util/props.js @@ -1,8 +1,6 @@ import { warn } from './warn' -const numberRegex = /^-?([0-9]+|[0-9]*\.[0-9]+)$/ - export function resolveProps (route, component, config) { switch (typeof config) { @@ -19,22 +17,7 @@ export function resolveProps (route, component, config) { if (!config) { return } - if (!component.props) { - return - } - const props = {} - for (const prop in route.params) { - const value = route.params[prop] - if (typeof component.props[prop] !== 'undefined') { - const propType = component.props[prop].type - if (propType === null || propType === String) { - props[prop] = value - } else if (propType === Number && value.match(numberRegex)) { - props[prop] = parseFloat(value) - } - } - } - return props + 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/specs/route-props.js b/test/e2e/specs/route-props.js index 9461dff22..7327be96e 100644 --- a/test/e2e/specs/route-props.js +++ b/test/e2e/specs/route-props.js @@ -3,7 +3,7 @@ module.exports = { browser .url('http://localhost:8080/route-props/') .waitForElementVisible('#app', 1000) - .assert.count('li a', 5) + .assert.count('li a', 4) .assert.urlEquals('http://localhost:8080/route-props/') .assert.containsText('.hello', 'Hello Vue!') @@ -20,10 +20,6 @@ module.exports = { .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() } } From 5ff882ed25a2ea8c75708c04e7030b73c9e41a0b Mon Sep 17 00:00:00 2001 From: Bob Fanger Date: Fri, 2 Dec 2016 15:39:18 +0100 Subject: [PATCH 5/6] Updated docs --- docs/en/essentials/passing-props.md | 4 ++-- examples/route-props/app.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/essentials/passing-props.md b/docs/en/essentials/passing-props.md index e0af0b375..87185977c 100644 --- a/docs/en/essentials/passing-props.md +++ b/docs/en/essentials/passing-props.md @@ -52,7 +52,7 @@ const router = new VueRouter({ ### Function mode You can create a function that returns props. -This allows you to mix static values and values based on the route. +This allows you to to cast the parameter to another type, mix static values and route-based values, etc. ``` js const router = new VueRouter({ @@ -65,7 +65,7 @@ const router = new VueRouter({ 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. +If route and state detemine the props on use wrapper component, 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/route-props/app.js b/examples/route-props/app.js index 906fa1de6..d39e682b3 100644 --- a/examples/route-props/app.js +++ b/examples/route-props/app.js @@ -16,7 +16,7 @@ const router = new VueRouter({ 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: '/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 ] From 0ac46ca8d8ced66ffbaed4fd6826ee6f37f8bf81 Mon Sep 17 00:00:00 2001 From: Bob Fanger Date: Sun, 15 Jan 2017 22:09:49 +0100 Subject: [PATCH 6/6] Updated docs + re-enabled props functionality --- docs/en/essentials/passing-props.md | 7 ++++--- src/components/view.js | 2 +- test/e2e/nightwatch.config.js | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/en/essentials/passing-props.md b/docs/en/essentials/passing-props.md index 87185977c..b6e20bdf2 100644 --- a/docs/en/essentials/passing-props.md +++ b/docs/en/essentials/passing-props.md @@ -40,11 +40,12 @@ 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: '/user/first-one', component: User, props: {id: '1' } } + { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } } ] }) ``` @@ -52,7 +53,7 @@ const router = new VueRouter({ ### Function mode You can create a function that returns props. -This allows you to to cast the parameter to another type, mix static values and route-based values, etc. +This allows you to to cast the parameter to another type, combine static values with route-based values, etc. ``` js const router = new VueRouter({ @@ -65,7 +66,7 @@ const router = new VueRouter({ 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. -If route and state detemine the props on use wrapper component, that way vue can react to state 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/src/components/view.js b/src/components/view.js index 4c83c314f..5cb8e9e67 100644 --- a/src/components/view.js +++ b/src/components/view.js @@ -57,8 +57,8 @@ export default { if (matched.instances[name] === vnode.child) { matched.instances[name] = undefined } - data.props = resolveProps(route, component, matched.props && matched.props[name]) } + data.props = resolveProps(route, component, matched.props && matched.props[name]) return h(component, data, children) } diff --git a/test/e2e/nightwatch.config.js b/test/e2e/nightwatch.config.js index fadfca353..08daf7ab8 100644 --- a/test/e2e/nightwatch.config.js +++ b/test/e2e/nightwatch.config.js @@ -1,6 +1,4 @@ // http://nightwatchjs.org/guide#settings-file -var seleniumServer = require('selenium-server'); -var chromedriver = require('chromedriver'); module.exports = { 'src_folders': ['test/e2e/specs'], @@ -10,7 +8,7 @@ module.exports = { 'selenium': { 'start_process': true, - 'server_path': seleniumServer.path, + 'server_path': require('selenium-server').path, 'host': '127.0.0.1', 'port': 4444, 'cli_args': {