From 5fecbd25e294ad7279a9086fdda67330e19ffe07 Mon Sep 17 00:00:00 2001 From: 38elements <38elements@users.noreply.github.com> Date: Mon, 23 Jul 2018 02:58:03 +0900 Subject: [PATCH] fix: improve slots option (#813) --- packages/create-instance/add-slots.js | 39 --------- .../create-functional-component.js | 4 +- packages/create-instance/create-instance.js | 17 +++- .../create-instance/create-slot-vnodes.js | 56 ++++++++++++ .../components/component-with-parent-name.vue | 18 ++++ test/specs/mounting-options/slots.spec.js | 86 ++++++++++++++++++- 6 files changed, 176 insertions(+), 44 deletions(-) delete mode 100644 packages/create-instance/add-slots.js create mode 100644 packages/create-instance/create-slot-vnodes.js create mode 100644 test/resources/components/component-with-parent-name.vue diff --git a/packages/create-instance/add-slots.js b/packages/create-instance/add-slots.js deleted file mode 100644 index 86f430f82..000000000 --- a/packages/create-instance/add-slots.js +++ /dev/null @@ -1,39 +0,0 @@ -// @flow - -import { compileToFunctions } from 'vue-template-compiler' - -function startsWithTag (str: SlotValue): boolean { - return typeof str === 'string' && str.trim()[0] === '<' -} - -function createVNodesForSlot ( - h: Function, - slotValue: SlotValue, - name: string -): VNode | string { - if (typeof slotValue === 'string' && !startsWithTag(slotValue)) { - return slotValue - } - - const el = - typeof slotValue === 'string' ? compileToFunctions(slotValue) : slotValue - - const vnode = h(el) - vnode.data.slot = name - return vnode -} - -export function createSlotVNodes ( - h: Function, - slots: SlotsObject -): Array { - return Object.keys(slots).reduce((acc, key) => { - const content = slots[key] - if (Array.isArray(content)) { - const nodes = content.map(slotDef => createVNodesForSlot(h, slotDef, key)) - return acc.concat(nodes) - } - - return acc.concat(createVNodesForSlot(h, content, key)) - }, []) -} diff --git a/packages/create-instance/create-functional-component.js b/packages/create-instance/create-functional-component.js index ee3feb392..8eb9b88cc 100644 --- a/packages/create-instance/create-functional-component.js +++ b/packages/create-instance/create-functional-component.js @@ -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, @@ -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, diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index 515ddeff9..a70f92f6a 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -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' @@ -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, @@ -109,6 +120,8 @@ export default function createInstance ( }) if (options.slots) { + compileTemplateForSlots(options.slots) + // $FlowIgnore validateSlots(options.slots) } @@ -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, diff --git a/packages/create-instance/create-slot-vnodes.js b/packages/create-instance/create-slot-vnodes.js new file mode 100644 index 000000000..b73bf95b6 --- /dev/null +++ b/packages/create-instance/create-slot-vnodes.js @@ -0,0 +1,56 @@ +// @flow + +import { compileToFunctions } from 'vue-template-compiler' + +function createVNodes ( + vm: Component, + slotValue: string +): Array { + const el = compileToFunctions(`
${slotValue}
`) + 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 { + 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)) + }, []) +} diff --git a/test/resources/components/component-with-parent-name.vue b/test/resources/components/component-with-parent-name.vue new file mode 100644 index 000000000..17524d66f --- /dev/null +++ b/test/resources/components/component-with-parent-name.vue @@ -0,0 +1,18 @@ + + + diff --git a/test/specs/mounting-options/slots.spec.js b/test/specs/mounting-options/slots.spec.js index 19e338b05..1c77c69d4 100644 --- a/test/specs/mounting-options/slots.spec.js +++ b/test/specs/mounting-options/slots.spec.js @@ -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', () => { @@ -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,', @@ -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', @@ -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: '
', + data () { + return { + childComponentName: '' + } + } + }, + { + components: { + ComponentWithParentName + }, + slots: { + default: [ + '', + '' + ] + }, + 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('
FOO,quux
FOO,quux
') + + ParentComponent = mount( + { + name: 'parentComponent', + template: '
', + data () { + return { + childComponentName: '' + } + } + }, + { + slots: { + default: { + name: childComponentName, + template: '

1234

', + 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('

1234

') + } + ) })