Skip to content
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

Feat: Implement Scoped Slot string support #115

Merged
merged 11 commits into from
May 12, 2020
2 changes: 2 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function createEntry(options) {
input,
external: [
'vue',
'@vue/compiler-dom',
'lodash/mergeWith',
'lodash/isString'
],
Expand All @@ -41,6 +42,7 @@ function createEntry(options) {
format,
globals: {
vue: 'Vue',
'@vue/compiler-dom': 'VueCompilerDOM',
'lodash/mergeWith': '_.mergeWith',
'lodash/isString': '_.isString',
}
Expand Down
21 changes: 18 additions & 3 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { config } from './config'
import { GlobalMountOptions } from './types'
import { mergeGlobalProperties, isString } from './utils'
import { processSlot } from './utils/compileSlots'
import { createWrapper, VueWrapper } from './vue-wrapper'
import { attachEmitListener } from './emitMixin'
import { createDataMixin } from './dataMixin'
Expand All @@ -26,8 +27,9 @@ import {
MOUNT_PARENT_NAME
} from './constants'
import { stubComponents } from './stubs'
import { parse } from '@vue/compiler-dom'

type Slot = VNode | string | { render: Function }
type Slot = VNode | string | { render: Function } | Function

interface MountingOptions<Props> {
data?: () => Record<string, unknown>
Expand Down Expand Up @@ -103,8 +105,21 @@ export function mount(
return acc
}

acc[name] = () => slot
return acc
if (typeof slot === 'function') {
acc[name] = slot
return acc
}

if (typeof slot === 'object') {
acc[name] = () => slot
return acc
}

if (typeof slot === 'string') {
// slot is most probably a scoped slot string or a plain string
acc[name] = (props) => h(processSlot(slot), props)
return acc
}
}, {})

// override component data with mounting options data
Expand Down
40 changes: 40 additions & 0 deletions src/utils/compileSlots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { compile } from '@vue/compiler-dom'

export function processSlot(template = '', Vue = require('vue')) {
const hasWrappingTemplate = template && template.startsWith('<template')

// allow content without `template` tag, for easier testing
if (!hasWrappingTemplate) {
template = `<template #default="params">${template}</template>`
}

const { code } = compile(
`<SlotWrapper v-bind="$attrs">${template}</SlotWrapper>`,
{
mode: 'function',
prefixIdentifiers: true
}
)
const createRenderFunction = new Function('Vue', `'use strict';\n${code}`)

return {
inheritAttrs: false,
render: createRenderFunction(Vue),
components: {
SlotWrapper: {
inheritAttrs: false,
setup(_, { slots, attrs }) {
return () => {
const names = Object.keys(slots)
if (names.length === 0) {
return []
} else {
const slotName = names[0]
return slots[slotName](attrs)
}
}
}
}
}
}
}
19 changes: 19 additions & 0 deletions tests/components/ComponentWithSlots.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
<template>
<div class="ComponentWithSlots">
<div class="default">
<slot />
</div>
<div class="named">
<slot name="named" />
</div>
<div class="withDefault">
<slot name="withDefault">With Default Content</slot>
</div>
<div class="scoped">
<slot name="scoped" v-bind="{ boolean, string, object }" />
</div>
<div class="scopedWithDefault">
<slot name="scopedWithDefault" v-bind="{ boolean, string, object }">
boolean: {{ boolean }}
string: {{ string }}
object: {{ object }}
</slot>
</div>
<slot />
<slot name="named" />
<slot name="withDefault">With Default Content</slot>
Expand Down
189 changes: 138 additions & 51 deletions tests/mountingOptions/slots.spec.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,163 @@
import { defineComponent, h } from 'vue'
import { h } from 'vue'

import { mount } from '../../src'
import WithProps from '../components/WithProps.vue'
import Hello from '../components/Hello.vue'
import ComponentWithSlots from '../components/ComponentWithSlots.vue'

describe('slots', () => {
it('supports default slot', () => {
const ItemWithSlots = defineComponent({
name: 'ItemWithSlots',
render() {
return h('div', {}, this.$slots.default())
}
describe('normal slots', () => {
it('supports providing a plain string text in slot', () => {
const defaultString = 'Rendered in Default'
let namedString = 'Rendered in Named'
const wrapper = mount(ComponentWithSlots, {
slots: {
default: defaultString,
named: namedString
}
})
expect(wrapper.find('.default').text()).toBe(defaultString)
expect(wrapper.find('.named').text()).toBe(namedString)
})

const wrapper = mount(ItemWithSlots, {
slots: {
default: h('span', {}, 'Default Slot')
}
it('supports providing an html string into a slot', () => {
const defaultSlot = '<div><p class="defaultNested">Content</p></div>'
const namedSlot = '<div><p class="namedNested">Content</p></div>'

const wrapper = mount(ComponentWithSlots, {
slots: {
default: defaultSlot,
named: namedSlot
}
})

expect(wrapper.find('.defaultNested').exists()).toBe(true)
expect(wrapper.find('.namedNested').exists()).toBe(true)
})

expect(wrapper.html()).toBe('<div><span>Default Slot</span></div>')
})
it('supports providing a render function to slot', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
default: h('span', {}, 'Default'),
named: h('span', {}, 'Named')
}
})

it('supports named slots', () => {
const ItemWithNamedSlot = defineComponent({
render() {
return h('div', {}, this.$slots.foo())
}
expect(wrapper.find('.default').html()).toEqual(
'<div class="default"><span>Default</span></div>'
)
expect(wrapper.find('.named').html()).toEqual(
'<div class="named"><span>Named</span></div>'
)
})

const wrapper = mount(ItemWithNamedSlot, {
slots: {
foo: h('span', {}, 'Foo')
}
it('does not render slots that do not exist', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
notExisting: () => h('span', {}, 'NotExistingText')
}
})

expect(wrapper.text()).not.toContain('NotExistingText')
})

expect(wrapper.html()).toBe('<div><span>Foo</span></div>')
it('supports passing a SFC', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
named: Hello
}
})

expect(wrapper.find('.named').html()).toBe(
'' +
'<div class="named">' +
'<div id="root">' +
'<div id="msg"></div>' +
'</div>' +
'</div>'
)
})
})

it('supports default and named slots together', () => {
const Component = defineComponent({
render() {
return h('div', {}, [
h('div', {}, this.$slots.foo()),
h('div', {}, this.$slots.default())
])
}
describe('scoped slots', () => {
it('allows providing a plain text string', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: 'Just a plain string'
}
})
expect(wrapper.find('.scoped').text()).toEqual('Just a plain string')
})

const wrapper = mount(Component, {
slots: {
default: 'Default',
foo: h('h1', {}, 'Named Slot')
}
it('allows passing a function that returns a render function', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: (params) => h('div', {}, JSON.stringify(params))
}
})

expect(wrapper.find('.scoped').text()).toEqual(
'{"boolean":true,"string":"string","object":{"foo":"foo"}}'
)
})

expect(wrapper.html()).toBe(
'<div><div><h1>Named Slot</h1></div><div>Default</div></div>'
)
})
it('allows passing a function to store variables for assertion', () => {
let assertParams

it('supports passing a SFC', () => {
const wrapper = mount(
{
template: `<div><slot name="foo" msg="Hello" /></div>`
},
{
const wrapper = mount(ComponentWithSlots, {
slots: {
foo: WithProps
scoped: (params) => {
assertParams = params
// always return something
return 'foo'
}
}
}
)
})

expect(wrapper.html()).toBe('<div><p>Hello</p></div>')
expect(assertParams).toEqual({
boolean: true,
string: 'string',
object: { foo: 'foo' }
})
})

it('allows passing a scoped slot via string with no destructuring using the # syntax', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: `<template #scoped="params"><div>Just a plain {{ params.boolean }} {{ params.string }}</div></template>`
}
})

expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
})

it('allows passing a scoped slot via a string with destructuring using the # syntax', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: `<template #scoped="{string, boolean}"><div>Just a plain {{ boolean }} {{ string }}</div></template>`
}
})

expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
})

it('allows passing a scoped slot via string with no destructuring using the v-slot syntax ', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: `<template v-slot:scoped="params"><div>Just a plain {{ params.boolean }} {{ params.string }}</div></template>`
}
})

expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
})

it('allows passing a scoped slot via string with no destructuring without template tag', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: `<div>Just a plain {{ params.boolean }} {{ params.string }}</div>`
}
})

expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
})
})
})