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

fix: improve slots option #813

Merged
merged 9 commits into from
Jul 22, 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
39 changes: 0 additions & 39 deletions packages/create-instance/add-slots.js

This file was deleted.

4 changes: 2 additions & 2 deletions packages/create-instance/create-functional-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { throwError } from 'shared/util'
import { validateSlots } from './validate-slots'
import { createSlotVNodes } from './add-slots'
import { createSlotVNodes } from './create-slot-vnodes'

export default function createFunctionalComponent (
component: Component,
Expand All @@ -25,7 +25,7 @@ export default function createFunctionalComponent (
mountingOptions.context.children.map(
x => (typeof x === 'function' ? x(h) : x)
)) ||
createSlotVNodes(h, mountingOptions.slots || {})
createSlotVNodes(this, mountingOptions.slots || {})
)
},
name: component.name,
Expand Down
17 changes: 15 additions & 2 deletions packages/create-instance/create-instance.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow

import { createSlotVNodes } from './add-slots'
import { createSlotVNodes } from './create-slot-vnodes'
import addMocks from './add-mocks'
import { addEventLogger } from './log-events'
import { createComponentStubs } from 'shared/stub-components'
Expand All @@ -12,6 +12,17 @@ import { componentNeedsCompiling } from 'shared/validators'
import { validateSlots } from './validate-slots'
import createScopedSlots from './create-scoped-slots'

function compileTemplateForSlots (slots: Object): void {
Object.keys(slots).forEach(key => {
const slot = Array.isArray(slots[key]) ? slots[key] : [slots[key]]
slot.forEach(slotValue => {
if (componentNeedsCompiling(slotValue)) {
compileTemplate(slotValue)
}
})
})
}

export default function createInstance (
component: Component,
options: Options,
Expand Down Expand Up @@ -109,6 +120,8 @@ export default function createInstance (
})

if (options.slots) {
compileTemplateForSlots(options.slots)
// $FlowIgnore
validateSlots(options.slots)
}

Expand All @@ -129,7 +142,7 @@ export default function createInstance (
provide: options.provide,
render (h) {
const slots = options.slots
? createSlotVNodes(h, options.slots)
? createSlotVNodes(this, options.slots)
: undefined
return h(
Constructor,
Expand Down
56 changes: 56 additions & 0 deletions packages/create-instance/create-slot-vnodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// @flow

import { compileToFunctions } from 'vue-template-compiler'

function createVNodes (
vm: Component,
slotValue: string
): Array<VNode> {
const el = compileToFunctions(`<div>${slotValue}</div>`)
const _staticRenderFns = vm._renderProxy.$options.staticRenderFns
// version < 2.5
if (!vm._renderProxy._staticTrees) {
vm._renderProxy._staticTrees = []
}
vm._renderProxy.$options.staticRenderFns = el.staticRenderFns
const vnode = el.render.call(vm._renderProxy, vm.$createElement)
vm._renderProxy.$options.staticRenderFns = _staticRenderFns
return vnode.children
}

function createVNodesForSlot (
vm: Component,
slotValue: SlotValue,
name: string,
): VNode | string {
let vnode
if (typeof slotValue === 'string') {
const vnodes = createVNodes(vm, slotValue)
vnode = vnodes[0]
} else {
vnode = vm.$createElement(slotValue)
}
if (vnode.data) {
vnode.data.slot = name
} else {
vnode.data = { slot: name }
}
return vnode
}

export function createSlotVNodes (
vm: Component,
slots: SlotsObject
): Array<VNode | string> {
return Object.keys(slots).reduce((acc, key) => {
const content = slots[key]
if (Array.isArray(content)) {
const nodes = content.map(
slotDef => createVNodesForSlot(vm, slotDef, key)
)
return acc.concat(nodes)
}

return acc.concat(createVNodesForSlot(vm, content, key))
}, [])
}
18 changes: 18 additions & 0 deletions test/resources/components/component-with-parent-name.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div><span baz="qux">{{ fromLocalVue }},{{ bar }}</span></div>
</template>

<script>
export default{
name: 'component-with-parent-name',
props: ['fromLocalVue'],
data () {
return {
bar: 'quux'
}
},
mounted () {
this.$parent.childComponentName = this.$options.name
}
}
</script>
86 changes: 85 additions & 1 deletion test/specs/mounting-options/slots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { compileToFunctions } from 'vue-template-compiler'
import Component from '~resources/components/component.vue'
import ComponentWithSlots from '~resources/components/component-with-slots.vue'
import ComponentAsAClass from '~resources/components/component-as-a-class.vue'
import ComponentWithParentName from '~resources/components/component-with-parent-name.vue'
import { describeWithMountingMethods, vueVersion } from '~resources/utils'
import { itSkipIf, itDoNotRunIf } from 'conditional-specs'
import { mount, createLocalVue } from '~vue/test-utils'

describeWithMountingMethods('options.slots', mountingMethod => {
it('mounts component with default slot if passed component in slot object', () => {
Expand Down Expand Up @@ -221,7 +223,7 @@ describeWithMountingMethods('options.slots', mountingMethod => {
}
})

it('mounts component with text slot', () => {
it('mounts component with default and named text slot', () => {
const wrapper = mountingMethod(ComponentWithSlots, {
slots: {
default: 'hello,',
Expand All @@ -235,6 +237,24 @@ describeWithMountingMethods('options.slots', mountingMethod => {
}
})

it('mounts functional component with only named text slot', () => {
const TestComponent = {
name: 'component-with-slots',
functional: true,
render: (h, ctx) => h('div', ctx.data, [ctx.slots().default, ctx.slots().footer])
}
const wrapper = mountingMethod(TestComponent, {
slots: {
footer: 'foo'
}
})
if (mountingMethod.name === 'renderToString') {
expect(wrapper).contains('foo')
} else {
expect(wrapper.text()).to.equal('foo')
}
})

it('mounts functional component with text slot', () => {
const TestComponent = {
name: 'component-with-slots',
Expand Down Expand Up @@ -568,4 +588,68 @@ describeWithMountingMethods('options.slots', mountingMethod => {
expect(wrapper.contains(ComponentAsAClass)).to.equal(true)
}
})

itDoNotRunIf(
mountingMethod.name === 'renderToString',
'sets a component which can access the parent component and the child component',
() => {
const childComponentName = 'component-with-parent-name'
const localVue = createLocalVue()
localVue.prototype.bar = 'FOO'
let ParentComponent = mount(
{
name: 'parentComponent',
template: '<div><slot /></div>',
data () {
return {
childComponentName: ''
}
}
},
{
components: {
ComponentWithParentName
},
slots: {
default: [
'<component-with-parent-name :fromLocalVue="bar" />',
'<component-with-parent-name :fromLocalVue="bar" />'
]
},
localVue
}
)
expect(ParentComponent.vm.childComponentName).to.equal(childComponentName)
expect(ParentComponent.vm.$children.length).to.equal(2)
expect(ParentComponent.vm.$children.every(c => c.$options.name === childComponentName)).to.equal(true)
expect(ParentComponent.html()).to.equal('<div><div><span baz="qux">FOO,quux</span></div><div><span baz="qux">FOO,quux</span></div></div>')

ParentComponent = mount(
{
name: 'parentComponent',
template: '<div><slot /></div>',
data () {
return {
childComponentName: ''
}
}
},
{
slots: {
default: {
name: childComponentName,
template: '<p>1234</p>',
mounted () {
this.$parent.childComponentName = this.$options.name
}
}
}
}
)
expect(ParentComponent.vm.childComponentName).to.equal(childComponentName)
expect(ParentComponent.vm.$children.length).to.equal(1)
expect(ParentComponent.vm.$children.every(c => c.$options.name === childComponentName)).to.equal(true)
expect(ParentComponent.html()).to.equal('<div><p>1234</p></div>')
}
)
})