Skip to content

Commit bcb7667

Browse files
committed
chore: merge in master
2 parents b871b58 + 418aff7 commit bcb7667

File tree

7 files changed

+281
-117
lines changed

7 files changed

+281
-117
lines changed

package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"@types/estree": "^0.0.42",
2424
"@types/jest": "^24.9.1",
2525
"@types/node": "12.12.35",
26-
"@vue/compiler-dom": "^3.0.0-beta.10",
27-
"@vue/compiler-sfc": "^3.0.0-beta.10",
26+
"@vue/compiler-dom": "^3.0.0-beta.12",
27+
"@vue/compiler-sfc": "^3.0.0-beta.12",
2828
"babel-jest": "^25.2.3",
2929
"babel-preset-jest": "^25.2.1",
3030
"dom-event-types": "^1.0.0",
@@ -40,14 +40,14 @@
4040
"ts-jest": "^25.0.0",
4141
"tsd": "0.11.0",
4242
"typescript": "^3.7.5",
43-
"vue": "^3.0.0-beta.10",
43+
"vue": "^3.0.0-beta.12",
4444
"vue-jest": "vuejs/vue-jest#next",
4545
"vuex": "^4.0.0-beta.1"
4646
},
4747
"peerDependencies": {
48-
"@vue/compiler-dom": "^3.0.0-beta.10",
49-
"@vue/compiler-sfc": "^3.0.0-beta.10",
50-
"vue": "^3.0.0-beta.10"
48+
"@vue/compiler-dom": "^3.0.0-beta.12",
49+
"@vue/compiler-sfc": "^3.0.0-beta.12",
50+
"vue": "^3.0.0-beta.12"
5151
},
5252
"author": {
5353
"name": "Lachlan Miller",

rollup.config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function createEntry(options) {
2525
input,
2626
external: [
2727
'vue',
28-
'@vue/compiler-dom'
28+
'@vue/compiler-dom',
2929
],
3030
plugins: [
3131
replace({
@@ -40,7 +40,7 @@ function createEntry(options) {
4040
format,
4141
globals: {
4242
vue: 'Vue',
43-
'@vue/compiler-dom': 'VueCompilerDOM'
43+
'@vue/compiler-dom': 'VueCompilerDOM',
4444
}
4545
}
4646
}

src/mount.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { config } from './config'
1818
import { GlobalMountOptions } from './types'
1919
import { mergeGlobalProperties, isString } from './utils'
20+
import { processSlot } from './utils/compileSlots'
2021
import { createWrapper, VueWrapper } from './vue-wrapper'
2122
import { attachEmitListener } from './emitMixin'
2223
import { createDataMixin } from './dataMixin'
@@ -26,16 +27,20 @@ import {
2627
MOUNT_PARENT_NAME
2728
} from './constants'
2829
import { stubComponents } from './stubs'
30+
import { parse } from '@vue/compiler-dom'
2931

30-
type Slot = VNode | string | { render: Function }
32+
type Slot = VNode | string | { render: Function } | Function
33+
34+
type SlotDictionary = {
35+
[key: string]: Slot
36+
}
3137

3238
interface MountingOptions<Props> {
3339
data?: () => Record<string, unknown>
3440
props?: Props
3541
attrs?: Record<string, unknown>
36-
slots?: {
42+
slots?: SlotDictionary & {
3743
default?: Slot
38-
[key: string]: Slot
3944
}
4045
global?: GlobalMountOptions
4146
attachTo?: HTMLElement | string
@@ -103,8 +108,21 @@ export function mount(
103108
return acc
104109
}
105110

106-
acc[name] = () => slot
107-
return acc
111+
if (typeof slot === 'function') {
112+
acc[name] = slot
113+
return acc
114+
}
115+
116+
if (typeof slot === 'object') {
117+
acc[name] = () => slot
118+
return acc
119+
}
120+
121+
if (typeof slot === 'string') {
122+
// slot is most probably a scoped slot string or a plain string
123+
acc[name] = (props) => h(processSlot(slot), props)
124+
return acc
125+
}
108126
}, {})
109127

110128
// override component data with mounting options data

src/utils/compileSlots.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { compile } from '@vue/compiler-dom'
2+
3+
export function processSlot(template = '', Vue = require('vue')) {
4+
const hasWrappingTemplate = template && template.startsWith('<template')
5+
6+
// allow content without `template` tag, for easier testing
7+
if (!hasWrappingTemplate) {
8+
template = `<template #default="params">${template}</template>`
9+
}
10+
11+
const { code } = compile(
12+
`<SlotWrapper v-bind="$attrs">${template}</SlotWrapper>`,
13+
{
14+
mode: 'function',
15+
prefixIdentifiers: true
16+
}
17+
)
18+
const createRenderFunction = new Function('Vue', `'use strict';\n${code}`)
19+
20+
return {
21+
inheritAttrs: false,
22+
render: createRenderFunction(Vue),
23+
components: {
24+
SlotWrapper: {
25+
inheritAttrs: false,
26+
setup(_, { slots, attrs }) {
27+
return () => {
28+
const names = Object.keys(slots)
29+
if (names.length === 0) {
30+
return []
31+
} else {
32+
const slotName = names[0]
33+
return slots[slotName](attrs)
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}
40+
}

tests/components/ComponentWithSlots.vue

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
<template>
22
<div class="ComponentWithSlots">
3+
<div class="default">
4+
<slot />
5+
</div>
6+
<div class="named">
7+
<slot name="named" />
8+
</div>
9+
<div class="withDefault">
10+
<slot name="withDefault">With Default Content</slot>
11+
</div>
12+
<div class="scoped">
13+
<slot name="scoped" v-bind="{ boolean, string, object }" />
14+
</div>
15+
<div class="scopedWithDefault">
16+
<slot name="scopedWithDefault" v-bind="{ boolean, string, object }">
17+
boolean: {{ boolean }}
18+
string: {{ string }}
19+
object: {{ object }}
20+
</slot>
21+
</div>
322
<slot />
423
<slot name="named" />
524
<slot name="withDefault">With Default Content</slot>

tests/mountingOptions/slots.spec.ts

+138-51
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,163 @@
1-
import { defineComponent, h } from 'vue'
1+
import { h } from 'vue'
22

33
import { mount } from '../../src'
4-
import WithProps from '../components/WithProps.vue'
4+
import Hello from '../components/Hello.vue'
5+
import ComponentWithSlots from '../components/ComponentWithSlots.vue'
56

67
describe('slots', () => {
7-
it('supports default slot', () => {
8-
const ItemWithSlots = defineComponent({
9-
name: 'ItemWithSlots',
10-
render() {
11-
return h('div', {}, this.$slots.default())
12-
}
8+
describe('normal slots', () => {
9+
it('supports providing a plain string text in slot', () => {
10+
const defaultString = 'Rendered in Default'
11+
let namedString = 'Rendered in Named'
12+
const wrapper = mount(ComponentWithSlots, {
13+
slots: {
14+
default: defaultString,
15+
named: namedString
16+
}
17+
})
18+
expect(wrapper.find('.default').text()).toBe(defaultString)
19+
expect(wrapper.find('.named').text()).toBe(namedString)
1320
})
1421

15-
const wrapper = mount(ItemWithSlots, {
16-
slots: {
17-
default: h('span', {}, 'Default Slot')
18-
}
22+
it('supports providing an html string into a slot', () => {
23+
const defaultSlot = '<div><p class="defaultNested">Content</p></div>'
24+
const namedSlot = '<div><p class="namedNested">Content</p></div>'
25+
26+
const wrapper = mount(ComponentWithSlots, {
27+
slots: {
28+
default: defaultSlot,
29+
named: namedSlot
30+
}
31+
})
32+
33+
expect(wrapper.find('.defaultNested').exists()).toBe(true)
34+
expect(wrapper.find('.namedNested').exists()).toBe(true)
1935
})
2036

21-
expect(wrapper.html()).toBe('<div><span>Default Slot</span></div>')
22-
})
37+
it('supports providing a render function to slot', () => {
38+
const wrapper = mount(ComponentWithSlots, {
39+
slots: {
40+
default: h('span', {}, 'Default'),
41+
named: h('span', {}, 'Named')
42+
}
43+
})
2344

24-
it('supports named slots', () => {
25-
const ItemWithNamedSlot = defineComponent({
26-
render() {
27-
return h('div', {}, this.$slots.foo())
28-
}
45+
expect(wrapper.find('.default').html()).toEqual(
46+
'<div class="default"><span>Default</span></div>'
47+
)
48+
expect(wrapper.find('.named').html()).toEqual(
49+
'<div class="named"><span>Named</span></div>'
50+
)
2951
})
3052

31-
const wrapper = mount(ItemWithNamedSlot, {
32-
slots: {
33-
foo: h('span', {}, 'Foo')
34-
}
53+
it('does not render slots that do not exist', () => {
54+
const wrapper = mount(ComponentWithSlots, {
55+
slots: {
56+
notExisting: () => h('span', {}, 'NotExistingText')
57+
}
58+
})
59+
60+
expect(wrapper.text()).not.toContain('NotExistingText')
3561
})
3662

37-
expect(wrapper.html()).toBe('<div><span>Foo</span></div>')
63+
it('supports passing a SFC', () => {
64+
const wrapper = mount(ComponentWithSlots, {
65+
slots: {
66+
named: Hello
67+
}
68+
})
69+
70+
expect(wrapper.find('.named').html()).toBe(
71+
'' +
72+
'<div class="named">' +
73+
'<div id="root">' +
74+
'<div id="msg"></div>' +
75+
'</div>' +
76+
'</div>'
77+
)
78+
})
3879
})
3980

40-
it('supports default and named slots together', () => {
41-
const Component = defineComponent({
42-
render() {
43-
return h('div', {}, [
44-
h('div', {}, this.$slots.foo()),
45-
h('div', {}, this.$slots.default())
46-
])
47-
}
81+
describe('scoped slots', () => {
82+
it('allows providing a plain text string', () => {
83+
const wrapper = mount(ComponentWithSlots, {
84+
slots: {
85+
scoped: 'Just a plain string'
86+
}
87+
})
88+
expect(wrapper.find('.scoped').text()).toEqual('Just a plain string')
4889
})
4990

50-
const wrapper = mount(Component, {
51-
slots: {
52-
default: 'Default',
53-
foo: h('h1', {}, 'Named Slot')
54-
}
91+
it('allows passing a function that returns a render function', () => {
92+
const wrapper = mount(ComponentWithSlots, {
93+
slots: {
94+
scoped: (params) => h('div', {}, JSON.stringify(params))
95+
}
96+
})
97+
98+
expect(wrapper.find('.scoped').text()).toEqual(
99+
'{"boolean":true,"string":"string","object":{"foo":"foo"}}'
100+
)
55101
})
56102

57-
expect(wrapper.html()).toBe(
58-
'<div><div><h1>Named Slot</h1></div><div>Default</div></div>'
59-
)
60-
})
103+
it('allows passing a function to store variables for assertion', () => {
104+
let assertParams
61105

62-
it('supports passing a SFC', () => {
63-
const wrapper = mount(
64-
{
65-
template: `<div><slot name="foo" msg="Hello" /></div>`
66-
},
67-
{
106+
const wrapper = mount(ComponentWithSlots, {
68107
slots: {
69-
foo: WithProps
108+
scoped: (params) => {
109+
assertParams = params
110+
// always return something
111+
return 'foo'
112+
}
70113
}
71-
}
72-
)
114+
})
73115

74-
expect(wrapper.html()).toBe('<div><p>Hello</p></div>')
116+
expect(assertParams).toEqual({
117+
boolean: true,
118+
string: 'string',
119+
object: { foo: 'foo' }
120+
})
121+
})
122+
123+
it('allows passing a scoped slot via string with no destructuring using the # syntax', () => {
124+
const wrapper = mount(ComponentWithSlots, {
125+
slots: {
126+
scoped: `<template #scoped="params"><div>Just a plain {{ params.boolean }} {{ params.string }}</div></template>`
127+
}
128+
})
129+
130+
expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
131+
})
132+
133+
it('allows passing a scoped slot via a string with destructuring using the # syntax', () => {
134+
const wrapper = mount(ComponentWithSlots, {
135+
slots: {
136+
scoped: `<template #scoped="{string, boolean}"><div>Just a plain {{ boolean }} {{ string }}</div></template>`
137+
}
138+
})
139+
140+
expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
141+
})
142+
143+
it('allows passing a scoped slot via string with no destructuring using the v-slot syntax ', () => {
144+
const wrapper = mount(ComponentWithSlots, {
145+
slots: {
146+
scoped: `<template v-slot:scoped="params"><div>Just a plain {{ params.boolean }} {{ params.string }}</div></template>`
147+
}
148+
})
149+
150+
expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
151+
})
152+
153+
it('allows passing a scoped slot via string with no destructuring without template tag', () => {
154+
const wrapper = mount(ComponentWithSlots, {
155+
slots: {
156+
scoped: `<div>Just a plain {{ params.boolean }} {{ params.string }}</div>`
157+
}
158+
})
159+
160+
expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
161+
})
75162
})
76163
})

0 commit comments

Comments
 (0)