Skip to content

feat: add support for JSX and strings without a slot-scope attribute #871

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 8 commits into from
Jul 29, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"presets": ["env", "stage-2", "flow-vue"],
"plugins": ["transform-decorators-legacy"],
"plugins": ["transform-decorators-legacy", "transform-vue-jsx"],
"comments": false
}
48 changes: 35 additions & 13 deletions docs/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected]+.
```js
shallowMount(Component, {
scopedSlots: {
foo: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
})
```

* You can not use `<template>` 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: '<p>{{props.index}},{{props.text}}</p>'
}
})
```

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: '<p slot-scope="props">{{props.index}},{{props.text}}</p>'
foo (props) {
return <div>{ props.text }</div>
}
}
})
expect(wrapper.find('#fooWrapper').html()).toBe(
`<div id="fooWrapper"><p>0,text1</p><p>1,text2</p><p>2,text3</p></div>`
)
```

## stubs
Expand Down
2 changes: 1 addition & 1 deletion flow/options.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
64 changes: 34 additions & 30 deletions packages/create-instance/create-scoped-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ` + `[email protected]+.`)
if (vueVersion < 2.1) {
throwError(`the scopedSlots option is only supported in [email protected]+.`)
}
}

function validateTempldate (template: string): void {
if (template.trim().substr(0, 9) === '<template') {
throwError(
`the scopedSlots option does not support a template ` +
`tag as the root element.`
)
const slotScopeRe = /<[^>]+ slot-scope=\"(.+)\"/

// Hide warning about <template> disallowed as root element
function customWarn (msg) {
if (msg.indexOf('Cannot use <template> as component root element') === -1) {
console.error(msg)
}
}

export default function createScopedSlots (
scopedSlotsOption: ?{ [slotName: string]: string }
): { [slotName: string]: (props: Object) => VNode | Array<VNode>} {
scopedSlotsOption: ?{ [slotName: string]: string | Function }
): {
[slotName: string]: (props: Object) => VNode | Array<VNode>
} {
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 <template> is a root element
return Array.isArray(res) ? res[0] : res
}
}
return scopedSlots
Expand Down
121 changes: 73 additions & 48 deletions test/specs/mounting-options/scopedSlots.spec.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: '<div><slot name="single" :text="foo" :i="123"></slot></div>',
data: () => ({
foo: 'bar'
})
}, {
scopedSlots: {
single: '<template><p>{{props.text}},{{props.i}}</p></template>'
}
})
expect(wrapper.html()).to.equal('<div><p>bar,123</p></div>')
})

itDoNotRunIf(
vueVersion < 2.1,
'handles render functions', () => {
const wrapper = mountingMethod({
template: '<div><slot name="single" :text="foo" /></div>',
data: () => ({
foo: 'bar'
})
}, {
scopedSlots: {
single: function (props) {
return this.$createElement('p', props.text)
}
}
})
expect(wrapper.html()).to.equal('<div><p>bar</p></div>')
})

itDoNotRunIf(
vueVersion < 2.5,
'mounts component scoped slots in render function',
() => {
const destructuringWrapper = mountingMethod(
Expand All @@ -29,7 +62,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
{
scopedSlots: {
default:
'<p slot-scope="{ index, item }">{{index}},{{item}}</p>'
'<template slot-scope="{ index, item }"><p>{{index}},{{item}}</p></template>'
}
}
)
Expand All @@ -38,16 +71,16 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
const notDestructuringWrapper = mountingMethod(
{
render: function () {
return this.$scopedSlots.default({
return this.$scopedSlots.named({
index: 1,
item: 'foo'
})
}
},
{
scopedSlots: {
default:
'<p slot-scope="props">{{props.index}},{{props.item}}</p>'
named:
'<p slot-scope="foo">{{foo.index}},{{foo.item}}</p>'
}
}
)
Expand All @@ -56,15 +89,15 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
)

itDoNotRunIf(
vueVersion < 2.5 || isRunningPhantomJS,
vueVersion < 2.5,
'mounts component scoped slots',
() => {
const wrapper = mountingMethod(ComponentWithScopedSlots, {
slots: { default: '<span>123</span>' },
scopedSlots: {
destructuring:
'<p slot-scope="{ index, item }">{{index}},{{item}}</p>',
list: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>',
list: '<template slot-scope="foo"><p>{{foo.index}},{{foo.text}}</p></template>',
single: '<p slot-scope="bar">{{bar.text}}</p>',
noProps: '<p slot-scope="baz">baz</p>'
}
Expand Down Expand Up @@ -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: '<template></template>'
}
vueVersion < 2.5,
'handles JSX', () => {
const wrapper = mountingMethod({
template: '<div><slot name="single" :text="foo"></slot></div>',
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 <p>{ text }</p>
}
}
})
expect(wrapper.html()).to.equal('<div><p>bar</p></div>')
})

itDoNotRunIf(
vueVersion >= 2.5 || isRunningPhantomJS,
'throws exception when vue version < 2.5',
() => {
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
list: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
vueVersion < 2.5,
'handles no slot-scope', () => {
const wrapper = mountingMethod({
template: '<div><slot name="single" :text="foo" :i="123"></slot></div>',
data: () => ({
foo: 'bar'
})
}
const message =
'[vue-test-utils]: the scopedSlots option is only supported in [email protected]+.'
expect(fn)
.to.throw()
.with.property('message', message)
}
)
}, {
scopedSlots: {
single: '<p>{{props.text}},{{props.i}}</p>'
}
})
expect(wrapper.html()).to.equal('<div><p>bar,123</p></div>')
})

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: {
Expand All @@ -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 [email protected]+.'
expect(fn)
.to.throw()
.with.property('message', message)
Expand Down
Loading