Skip to content

Commit 13bcaeb

Browse files
authored
feat: add support for JSX scopedSlots value (#871)
1 parent d5fceb7 commit 13bcaeb

File tree

7 files changed

+162
-99
lines changed

7 files changed

+162
-99
lines changed

Diff for: .babelrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"presets": ["env", "stage-2", "flow-vue"],
3-
"plugins": ["transform-decorators-legacy"],
3+
"plugins": ["transform-decorators-legacy", "transform-vue-jsx"],
44
"comments": false
55
}

Diff for: docs/api/options.md

+35-13
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,52 @@ expect(wrapper.find('div')).toBe(true)
6969

7070
## scopedSlots
7171

72-
- type: `{ [name: string]: string }`
72+
- type: `{ [name: string]: string|Function }`
7373

74-
Provide an object of scoped slots contents to the component. The key corresponds to the slot name. The value can be a template string.
74+
Provide an object of scoped slots to the component. The key corresponds to the slot name.
7575

76-
There are three limitations.
76+
You can set the name of the props using the slot-scope attribute:
7777

78-
* This option is only supported in [email protected]+.
78+
```js
79+
shallowMount(Component, {
80+
scopedSlots: {
81+
foo: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
82+
}
83+
})
84+
```
7985

80-
* You can not use `<template>` tag as the root element in the `scopedSlots` option.
86+
Otherwise props are available as a `props` object when the slot is evaluated:
8187

82-
* This does not support PhantomJS.
83-
You can use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer) as an alternative.
88+
```js
89+
shallowMount(Component, {
90+
scopedSlots: {
91+
default: '<p>{{props.index}},{{props.text}}</p>'
92+
}
93+
})
94+
```
8495

85-
Example:
96+
You can also pass a function that takes the props as an argument:
8697

8798
```js
88-
const wrapper = shallowMount(Component, {
99+
shallowMount(Component, {
100+
scopedSlots: {
101+
foo: function (props) {
102+
return this.$createElement('div', props.index)
103+
}
104+
}
105+
})
106+
```
107+
108+
Or you can use JSX. If write JSX in a method, `this.$createElement` is auto-injected by babel-plugin-transform-vue-jsx:
109+
110+
```js
111+
shallowMount(Component, {
89112
scopedSlots: {
90-
foo: '<p slot-scope="props">{{props.index}},{{props.text}}</p>'
113+
foo (props) {
114+
return <div>{ props.text }</div>
115+
}
91116
}
92117
})
93-
expect(wrapper.find('#fooWrapper').html()).toBe(
94-
`<div id="fooWrapper"><p>0,text1</p><p>1,text2</p><p>2,text3</p></div>`
95-
)
96118
```
97119

98120
## stubs

Diff for: flow/options.flow.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ declare type Options = {
55
mocks?: Object,
66
methods?: { [key: string]: Function },
77
slots?: SlotsObject,
8-
scopedSlots?: { [key: string]: string },
8+
scopedSlots?: { [key: string]: string | Function },
99
localVue?: Component,
1010
provide?: Object,
1111
stubs?: Stubs,

Diff for: package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
"babel-core": "^6.26.0",
3232
"babel-eslint": "^8.2.2",
3333
"babel-loader": "^7.1.3",
34+
"babel-plugin-syntax-jsx": "^6.18.0",
3435
"babel-plugin-transform-decorators-legacy": "^1.3.4",
36+
"babel-plugin-transform-vue-jsx": "^3.7.0",
3537
"babel-polyfill": "^6.23.0",
3638
"babel-preset-env": "^1.6.0",
3739
"babel-preset-flow-vue": "^1.0.0",
@@ -61,12 +63,12 @@
6163
"rollup": "^0.58.2",
6264
"sinon": "^2.3.2",
6365
"sinon-chai": "^2.10.0",
64-
"vue": "^2.5.16",
66+
"vue": "2.5.16",
6567
"vue-class-component": "^6.1.2",
6668
"vue-loader": "^13.6.2",
6769
"vue-router": "^3.0.1",
68-
"vue-server-renderer": "^2.5.16",
69-
"vue-template-compiler": "^2.5.16",
70+
"vue-server-renderer": "2.5.16",
71+
"vue-template-compiler": "2.5.16",
7072
"vuepress": "^0.10.0",
7173
"vuepress-theme-vue": "^1.0.3",
7274
"vuetify": "^0.16.9",

Diff for: packages/create-instance/create-scoped-slots.js

+34-30
Original file line numberDiff line numberDiff line change
@@ -32,55 +32,59 @@ function getVueTemplateCompilerHelpers (): { [name: string]: Function } {
3232
names.forEach(name => {
3333
helpers[name] = vue._renderProxy[name]
3434
})
35+
helpers.$createElement = vue._renderProxy.$createElement
3536
return helpers
3637
}
3738

3839
function validateEnvironment (): void {
39-
if (window.navigator.userAgent.match(/PhantomJS/i)) {
40-
throwError(
41-
`the scopedSlots option does not support PhantomJS. ` +
42-
`Please use Puppeteer, or pass a component.`
43-
)
44-
}
45-
if (vueVersion < 2.5) {
46-
throwError(`the scopedSlots option is only supported in ` + `[email protected]+.`)
40+
if (vueVersion < 2.1) {
41+
throwError(`the scopedSlots option is only supported in [email protected]+.`)
4742
}
4843
}
4944

50-
function validateTempldate (template: string): void {
51-
if (template.trim().substr(0, 9) === '<template') {
52-
throwError(
53-
`the scopedSlots option does not support a template ` +
54-
`tag as the root element.`
55-
)
45+
const slotScopeRe = /<[^>]+ slot-scope=\"(.+)\"/
46+
47+
// Hide warning about <template> disallowed as root element
48+
function customWarn (msg) {
49+
if (msg.indexOf('Cannot use <template> as component root element') === -1) {
50+
console.error(msg)
5651
}
5752
}
5853

5954
export default function createScopedSlots (
60-
scopedSlotsOption: ?{ [slotName: string]: string }
61-
): { [slotName: string]: (props: Object) => VNode | Array<VNode>} {
55+
scopedSlotsOption: ?{ [slotName: string]: string | Function }
56+
): {
57+
[slotName: string]: (props: Object) => VNode | Array<VNode>
58+
} {
6259
const scopedSlots = {}
6360
if (!scopedSlotsOption) {
6461
return scopedSlots
6562
}
6663
validateEnvironment()
6764
const helpers = getVueTemplateCompilerHelpers()
68-
for (const name in scopedSlotsOption) {
69-
const template = scopedSlotsOption[name]
70-
validateTempldate(template)
71-
const render = compileToFunctions(template).render
72-
const domParser = new window.DOMParser()
73-
const _document = domParser.parseFromString(template, 'text/html')
74-
const slotScope = _document.body.firstChild.getAttribute(
75-
'slot-scope'
76-
)
77-
const isDestructuring = isDestructuringSlotScope(slotScope)
78-
scopedSlots[name] = function (props) {
79-
if (isDestructuring) {
80-
return render.call({ ...helpers, ...props })
65+
for (const scopedSlotName in scopedSlotsOption) {
66+
const slot = scopedSlotsOption[scopedSlotName]
67+
const isFn = typeof slot === 'function'
68+
// Type check to silence flow (can't use isFn)
69+
const renderFn = typeof slot === 'function'
70+
? slot
71+
: compileToFunctions(slot, { warn: customWarn }).render
72+
73+
const hasSlotScopeAttr = !isFn && slot.match(slotScopeRe)
74+
const slotScope = hasSlotScopeAttr && hasSlotScopeAttr[1]
75+
scopedSlots[scopedSlotName] = function (props) {
76+
let res
77+
if (isFn) {
78+
res = renderFn.call({ ...helpers }, props)
79+
} else if (slotScope && !isDestructuringSlotScope(slotScope)) {
80+
res = renderFn.call({ ...helpers, [slotScope]: props })
81+
} else if (slotScope && isDestructuringSlotScope(slotScope)) {
82+
res = renderFn.call({ ...helpers, ...props })
8183
} else {
82-
return render.call({ ...helpers, [slotScope]: props })
84+
res = renderFn.call({ ...helpers, props })
8385
}
86+
// res is Array if <template> is a root element
87+
return Array.isArray(res) ? res[0] : res
8488
}
8589
}
8690
return scopedSlots

Diff for: test/specs/mounting-options/scopedSlots.spec.js

+73-48
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
describeWithShallowAndMount,
3-
vueVersion,
4-
isRunningPhantomJS
3+
vueVersion
54
} from '~resources/utils'
65
import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue'
76
import { itDoNotRunIf } from 'conditional-specs'
@@ -14,7 +13,41 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
1413
})
1514

1615
itDoNotRunIf(
17-
vueVersion < 2.5 || isRunningPhantomJS,
16+
vueVersion < 2.1,
17+
'handles templates as the root node', () => {
18+
const wrapper = mountingMethod({
19+
template: '<div><slot name="single" :text="foo" :i="123"></slot></div>',
20+
data: () => ({
21+
foo: 'bar'
22+
})
23+
}, {
24+
scopedSlots: {
25+
single: '<template><p>{{props.text}},{{props.i}}</p></template>'
26+
}
27+
})
28+
expect(wrapper.html()).to.equal('<div><p>bar,123</p></div>')
29+
})
30+
31+
itDoNotRunIf(
32+
vueVersion < 2.1,
33+
'handles render functions', () => {
34+
const wrapper = mountingMethod({
35+
template: '<div><slot name="single" :text="foo" /></div>',
36+
data: () => ({
37+
foo: 'bar'
38+
})
39+
}, {
40+
scopedSlots: {
41+
single: function (props) {
42+
return this.$createElement('p', props.text)
43+
}
44+
}
45+
})
46+
expect(wrapper.html()).to.equal('<div><p>bar</p></div>')
47+
})
48+
49+
itDoNotRunIf(
50+
vueVersion < 2.5,
1851
'mounts component scoped slots in render function',
1952
() => {
2053
const destructuringWrapper = mountingMethod(
@@ -29,7 +62,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
2962
{
3063
scopedSlots: {
3164
default:
32-
'<p slot-scope="{ index, item }">{{index}},{{item}}</p>'
65+
'<template slot-scope="{ index, item }"><p>{{index}},{{item}}</p></template>'
3366
}
3467
}
3568
)
@@ -38,16 +71,16 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
3871
const notDestructuringWrapper = mountingMethod(
3972
{
4073
render: function () {
41-
return this.$scopedSlots.default({
74+
return this.$scopedSlots.named({
4275
index: 1,
4376
item: 'foo'
4477
})
4578
}
4679
},
4780
{
4881
scopedSlots: {
49-
default:
50-
'<p slot-scope="props">{{props.index}},{{props.item}}</p>'
82+
named:
83+
'<p slot-scope="foo">{{foo.index}},{{foo.item}}</p>'
5184
}
5285
}
5386
)
@@ -56,15 +89,15 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
5689
)
5790

5891
itDoNotRunIf(
59-
vueVersion < 2.5 || isRunningPhantomJS,
92+
vueVersion < 2.5,
6093
'mounts component scoped slots',
6194
() => {
6295
const wrapper = mountingMethod(ComponentWithScopedSlots, {
6396
slots: { default: '<span>123</span>' },
6497
scopedSlots: {
6598
destructuring:
6699
'<p slot-scope="{ index, item }">{{index}},{{item}}</p>',
67-
list: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>',
100+
list: '<template slot-scope="foo"><p>{{foo.index}},{{foo.text}}</p></template>',
68101
single: '<p slot-scope="bar">{{bar.text}}</p>',
69102
noProps: '<p slot-scope="baz">baz</p>'
70103
}
@@ -106,51 +139,43 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
106139
)
107140

108141
itDoNotRunIf(
109-
vueVersion < 2.5 || isRunningPhantomJS,
110-
'throws exception when it is seted to a template tag at top',
111-
() => {
112-
const fn = () => {
113-
mountingMethod(ComponentWithScopedSlots, {
114-
scopedSlots: {
115-
single: '<template></template>'
116-
}
142+
vueVersion < 2.5,
143+
'handles JSX', () => {
144+
const wrapper = mountingMethod({
145+
template: '<div><slot name="single" :text="foo"></slot></div>',
146+
data: () => ({
147+
foo: 'bar'
117148
})
118-
}
119-
const message =
120-
'[vue-test-utils]: the scopedSlots option does not support a template tag as the root element.'
121-
expect(fn)
122-
.to.throw()
123-
.with.property('message', message)
124-
}
125-
)
149+
}, {
150+
scopedSlots: {
151+
single ({ text }) {
152+
return <p>{ text }</p>
153+
}
154+
}
155+
})
156+
expect(wrapper.html()).to.equal('<div><p>bar</p></div>')
157+
})
126158

127159
itDoNotRunIf(
128-
vueVersion >= 2.5 || isRunningPhantomJS,
129-
'throws exception when vue version < 2.5',
130-
() => {
131-
const fn = () => {
132-
mountingMethod(ComponentWithScopedSlots, {
133-
scopedSlots: {
134-
list: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
135-
}
160+
vueVersion < 2.5,
161+
'handles no slot-scope', () => {
162+
const wrapper = mountingMethod({
163+
template: '<div><slot name="single" :text="foo" :i="123"></slot></div>',
164+
data: () => ({
165+
foo: 'bar'
136166
})
137-
}
138-
const message =
139-
'[vue-test-utils]: the scopedSlots option is only supported in [email protected]+.'
140-
expect(fn)
141-
.to.throw()
142-
.with.property('message', message)
143-
}
144-
)
167+
}, {
168+
scopedSlots: {
169+
single: '<p>{{props.text}},{{props.i}}</p>'
170+
}
171+
})
172+
expect(wrapper.html()).to.equal('<div><p>bar,123</p></div>')
173+
})
145174

146175
itDoNotRunIf(
147-
vueVersion < 2.5,
148-
'throws exception when using PhantomJS',
176+
vueVersion > 2.0,
177+
'throws exception when vue version < 2.1',
149178
() => {
150-
if (window.navigator.userAgent.match(/Chrome|PhantomJS/i)) {
151-
return
152-
}
153-
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
154179
const fn = () => {
155180
mountingMethod(ComponentWithScopedSlots, {
156181
scopedSlots: {
@@ -159,7 +184,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => {
159184
})
160185
}
161186
const message =
162-
'[vue-test-utils]: the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.'
187+
'[vue-test-utils]: the scopedSlots option is only supported in [email protected]+.'
163188
expect(fn)
164189
.to.throw()
165190
.with.property('message', message)

0 commit comments

Comments
 (0)