diff --git a/.babelrc b/.babelrc
index 9f1cf787f..4d755394a 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,5 @@
{
"presets": ["env", "stage-2", "flow-vue"],
- "plugins": ["transform-decorators-legacy"],
+ "plugins": ["transform-decorators-legacy", "transform-vue-jsx"],
"comments": false
}
diff --git a/docs/api/options.md b/docs/api/options.md
index 1707e78fb..c2c3b5148 100644
--- a/docs/api/options.md
+++ b/docs/api/options.md
@@ -69,30 +69,52 @@ expect(wrapper.find('div')).toBe(true)
## scopedSlots
-- type: `{ [name: string]: string }`
+- type: `{ [name: string]: string|Function }`
-Provide an object of scoped slots contents to the component. The key corresponds to the slot name. The value can be a template string.
+Provide an object of scoped slots to the component. The key corresponds to the slot name.
-There are three limitations.
+You can set the name of the props using the slot-scope attribute:
-* This option is only supported in vue@2.5+.
+```js
+shallowMount(Component, {
+ scopedSlots: {
+ foo: '
{{foo.index}},{{foo.text}}
'
+ }
+})
+```
-* You can not use `` tag as the root element in the `scopedSlots` option.
+Otherwise props are available as a `props` object when the slot is evaluated:
-* This does not support PhantomJS.
-You can use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer) as an alternative.
+```js
+shallowMount(Component, {
+ scopedSlots: {
+ default: '{{props.index}},{{props.text}}
'
+ }
+})
+```
-Example:
+You can also pass a function that takes the props as an argument:
```js
-const wrapper = shallowMount(Component, {
+shallowMount(Component, {
+ scopedSlots: {
+ foo: function (props) {
+ return this.$createElement('div', props.index)
+ }
+ }
+})
+```
+
+Or you can use JSX. If write JSX in a method, `this.$createElement` is auto-injected by babel-plugin-transform-vue-jsx:
+
+```js
+shallowMount(Component, {
scopedSlots: {
- foo: '{{props.index}},{{props.text}}
'
+ foo (props) {
+ return { props.text }
+ }
}
})
-expect(wrapper.find('#fooWrapper').html()).toBe(
- ``
-)
```
## stubs
diff --git a/flow/options.flow.js b/flow/options.flow.js
index d154c1f14..c95fb78bf 100644
--- a/flow/options.flow.js
+++ b/flow/options.flow.js
@@ -5,7 +5,7 @@ declare type Options = {
mocks?: Object,
methods?: { [key: string]: Function },
slots?: SlotsObject,
- scopedSlots?: { [key: string]: string },
+ scopedSlots?: { [key: string]: string | Function },
localVue?: Component,
provide?: Object,
stubs?: Stubs,
diff --git a/package.json b/package.json
index 91a211ea3..1af27ed8a 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,9 @@
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.3",
+ "babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
+ "babel-plugin-transform-vue-jsx": "^3.7.0",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-flow-vue": "^1.0.0",
@@ -61,12 +63,12 @@
"rollup": "^0.58.2",
"sinon": "^2.3.2",
"sinon-chai": "^2.10.0",
- "vue": "^2.5.16",
+ "vue": "2.5.16",
"vue-class-component": "^6.1.2",
"vue-loader": "^13.6.2",
"vue-router": "^3.0.1",
- "vue-server-renderer": "^2.5.16",
- "vue-template-compiler": "^2.5.16",
+ "vue-server-renderer": "2.5.16",
+ "vue-template-compiler": "2.5.16",
"vuepress": "^0.10.0",
"vuepress-theme-vue": "^1.0.3",
"vuetify": "^0.16.9",
diff --git a/packages/create-instance/create-scoped-slots.js b/packages/create-instance/create-scoped-slots.js
index 6211fd320..fe45fc102 100644
--- a/packages/create-instance/create-scoped-slots.js
+++ b/packages/create-instance/create-scoped-slots.js
@@ -32,55 +32,59 @@ function getVueTemplateCompilerHelpers (): { [name: string]: Function } {
names.forEach(name => {
helpers[name] = vue._renderProxy[name]
})
+ helpers.$createElement = vue._renderProxy.$createElement
return helpers
}
function validateEnvironment (): void {
- if (window.navigator.userAgent.match(/PhantomJS/i)) {
- throwError(
- `the scopedSlots option does not support PhantomJS. ` +
- `Please use Puppeteer, or pass a component.`
- )
- }
- if (vueVersion < 2.5) {
- throwError(`the scopedSlots option is only supported in ` + `vue@2.5+.`)
+ if (vueVersion < 2.1) {
+ throwError(`the scopedSlots option is only supported in vue@2.1+.`)
}
}
-function validateTempldate (template: string): void {
- if (template.trim().substr(0, 9) === ']+ slot-scope=\"(.+)\"/
+
+// Hide warning about disallowed as root element
+function customWarn (msg) {
+ if (msg.indexOf('Cannot use as component root element') === -1) {
+ console.error(msg)
}
}
export default function createScopedSlots (
- scopedSlotsOption: ?{ [slotName: string]: string }
-): { [slotName: string]: (props: Object) => VNode | Array} {
+ scopedSlotsOption: ?{ [slotName: string]: string | Function }
+): {
+ [slotName: string]: (props: Object) => VNode | Array
+} {
const scopedSlots = {}
if (!scopedSlotsOption) {
return scopedSlots
}
validateEnvironment()
const helpers = getVueTemplateCompilerHelpers()
- for (const name in scopedSlotsOption) {
- const template = scopedSlotsOption[name]
- validateTempldate(template)
- const render = compileToFunctions(template).render
- const domParser = new window.DOMParser()
- const _document = domParser.parseFromString(template, 'text/html')
- const slotScope = _document.body.firstChild.getAttribute(
- 'slot-scope'
- )
- const isDestructuring = isDestructuringSlotScope(slotScope)
- scopedSlots[name] = function (props) {
- if (isDestructuring) {
- return render.call({ ...helpers, ...props })
+ for (const scopedSlotName in scopedSlotsOption) {
+ const slot = scopedSlotsOption[scopedSlotName]
+ const isFn = typeof slot === 'function'
+ // Type check to silence flow (can't use isFn)
+ const renderFn = typeof slot === 'function'
+ ? slot
+ : compileToFunctions(slot, { warn: customWarn }).render
+
+ const hasSlotScopeAttr = !isFn && slot.match(slotScopeRe)
+ const slotScope = hasSlotScopeAttr && hasSlotScopeAttr[1]
+ scopedSlots[scopedSlotName] = function (props) {
+ let res
+ if (isFn) {
+ res = renderFn.call({ ...helpers }, props)
+ } else if (slotScope && !isDestructuringSlotScope(slotScope)) {
+ res = renderFn.call({ ...helpers, [slotScope]: props })
+ } else if (slotScope && isDestructuringSlotScope(slotScope)) {
+ res = renderFn.call({ ...helpers, ...props })
} else {
- return render.call({ ...helpers, [slotScope]: props })
+ res = renderFn.call({ ...helpers, props })
}
+ // res is Array if is a root element
+ return Array.isArray(res) ? res[0] : res
}
}
return scopedSlots
diff --git a/test/specs/mounting-options/scopedSlots.spec.js b/test/specs/mounting-options/scopedSlots.spec.js
index 05259800d..b1b651141 100644
--- a/test/specs/mounting-options/scopedSlots.spec.js
+++ b/test/specs/mounting-options/scopedSlots.spec.js
@@ -1,7 +1,6 @@
import {
describeWithShallowAndMount,
- vueVersion,
- isRunningPhantomJS
+ vueVersion
} from '~resources/utils'
import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue'
import { itDoNotRunIf } from 'conditional-specs'
@@ -14,7 +13,41 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
})
itDoNotRunIf(
- vueVersion < 2.5 || isRunningPhantomJS,
+ vueVersion < 2.1,
+ 'handles templates as the root node', () => {
+ const wrapper = mountingMethod({
+ template: '
',
+ data: () => ({
+ foo: 'bar'
+ })
+ }, {
+ scopedSlots: {
+ single: '{{props.text}},{{props.i}}
'
+ }
+ })
+ expect(wrapper.html()).to.equal('')
+ })
+
+ itDoNotRunIf(
+ vueVersion < 2.1,
+ 'handles render functions', () => {
+ const wrapper = mountingMethod({
+ template: '
',
+ data: () => ({
+ foo: 'bar'
+ })
+ }, {
+ scopedSlots: {
+ single: function (props) {
+ return this.$createElement('p', props.text)
+ }
+ }
+ })
+ expect(wrapper.html()).to.equal('')
+ })
+
+ itDoNotRunIf(
+ vueVersion < 2.5,
'mounts component scoped slots in render function',
() => {
const destructuringWrapper = mountingMethod(
@@ -29,7 +62,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
{
scopedSlots: {
default:
- '{{index}},{{item}}
'
+ '{{index}},{{item}}
'
}
}
)
@@ -38,7 +71,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
const notDestructuringWrapper = mountingMethod(
{
render: function () {
- return this.$scopedSlots.default({
+ return this.$scopedSlots.named({
index: 1,
item: 'foo'
})
@@ -46,8 +79,8 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
},
{
scopedSlots: {
- default:
- '{{props.index}},{{props.item}}
'
+ named:
+ '{{foo.index}},{{foo.item}}
'
}
}
)
@@ -56,7 +89,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
)
itDoNotRunIf(
- vueVersion < 2.5 || isRunningPhantomJS,
+ vueVersion < 2.5,
'mounts component scoped slots',
() => {
const wrapper = mountingMethod(ComponentWithScopedSlots, {
@@ -64,7 +97,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
scopedSlots: {
destructuring:
'{{index}},{{item}}
',
- list: '{{foo.index}},{{foo.text}}
',
+ list: '{{foo.index}},{{foo.text}}
',
single: '{{bar.text}}
',
noProps: 'baz
'
}
@@ -106,51 +139,43 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
)
itDoNotRunIf(
- vueVersion < 2.5 || isRunningPhantomJS,
- 'throws exception when it is seted to a template tag at top',
- () => {
- const fn = () => {
- mountingMethod(ComponentWithScopedSlots, {
- scopedSlots: {
- single: ''
- }
+ vueVersion < 2.5,
+ 'handles JSX', () => {
+ const wrapper = mountingMethod({
+ template: '
',
+ data: () => ({
+ foo: 'bar'
})
- }
- const message =
- '[vue-test-utils]: the scopedSlots option does not support a template tag as the root element.'
- expect(fn)
- .to.throw()
- .with.property('message', message)
- }
- )
+ }, {
+ scopedSlots: {
+ single ({ text }) {
+ return { text }
+ }
+ }
+ })
+ expect(wrapper.html()).to.equal('')
+ })
itDoNotRunIf(
- vueVersion >= 2.5 || isRunningPhantomJS,
- 'throws exception when vue version < 2.5',
- () => {
- const fn = () => {
- mountingMethod(ComponentWithScopedSlots, {
- scopedSlots: {
- list: '{{foo.index}},{{foo.text}}
'
- }
+ vueVersion < 2.5,
+ 'handles no slot-scope', () => {
+ const wrapper = mountingMethod({
+ template: '
',
+ data: () => ({
+ foo: 'bar'
})
- }
- const message =
- '[vue-test-utils]: the scopedSlots option is only supported in vue@2.5+.'
- expect(fn)
- .to.throw()
- .with.property('message', message)
- }
- )
+ }, {
+ scopedSlots: {
+ single: '{{props.text}},{{props.i}}
'
+ }
+ })
+ expect(wrapper.html()).to.equal('')
+ })
itDoNotRunIf(
- vueVersion < 2.5,
- 'throws exception when using PhantomJS',
+ vueVersion > 2.0,
+ 'throws exception when vue version < 2.1',
() => {
- if (window.navigator.userAgent.match(/Chrome|PhantomJS/i)) {
- return
- }
- window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
@@ -159,7 +184,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
})
}
const message =
- '[vue-test-utils]: the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.'
+ '[vue-test-utils]: the scopedSlots option is only supported in vue@2.1+.'
expect(fn)
.to.throw()
.with.property('message', message)
diff --git a/yarn.lock b/yarn.lock
index b78e59e32..723355b1c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1536,6 +1536,10 @@ babel-plugin-syntax-flow@^6.18.0, babel-plugin-syntax-flow@^6.8.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+babel-plugin-syntax-jsx@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+
babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -1790,6 +1794,12 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
+babel-plugin-transform-vue-jsx@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-3.7.0.tgz#d40492e6692a36b594f7e9a1928f43e969740960"
+ dependencies:
+ esutils "^2.0.2"
+
babel-plugin-transform-vue-jsx@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-4.0.1.tgz#2c8bddce87a6ef09eaa59869ff1bfbeeafc5f88d"
@@ -9700,7 +9710,7 @@ vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
-vue-server-renderer@^2.5.16:
+vue-server-renderer@2.5.16, vue-server-renderer@^2.5.16:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.5.16.tgz#279ef8e37e502a0de3a9ae30758cc04a472eaac0"
dependencies:
@@ -9727,7 +9737,7 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.5.16:
+vue-template-compiler@2.5.16, vue-template-compiler@^2.5.16:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
dependencies:
@@ -9738,7 +9748,7 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
-vue@^2.5.16:
+vue@2.5.16, vue@^2.5.16:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"