diff --git a/packages/create-instance/create-functional-component.js b/packages/create-instance/create-functional-component.js index ee3feb392..04021ba23 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, diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index 515ddeff9..2d7437d3e 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -1,9 +1,9 @@ // @flow -import { createSlotVNodes } from './add-slots' import addMocks from './add-mocks' import { addEventLogger } from './log-events' import { createComponentStubs } from 'shared/stub-components' +import { createSlotVNodes } from './create-slot-vnodes' import { throwError, warn, vueVersion } from 'shared/util' import { compileTemplate } from 'shared/compile-template' import extractInstanceOptions from './extract-instance-options' diff --git a/packages/create-instance/create-render-slot.js b/packages/create-instance/create-render-slot.js new file mode 100644 index 000000000..c16baf5bd --- /dev/null +++ b/packages/create-instance/create-render-slot.js @@ -0,0 +1,64 @@ +// @flow + +import Vue from 'vue' +import { compileToFunctions } from 'vue-template-compiler' + +const _renderSlot = Vue.prototype._t + +function createVNodes ( + vm: Component, + slotValue: Component | string +): ?Array { + if (typeof slotValue === 'string') { + // Since compileToFunctions is checked in createSlotVNodes(), + // it is not necessary to check compileToFunctions. + const compiledResult = compileToFunctions(`
${slotValue}
`) + const _staticRenderFns = vm._renderProxy.$options.staticRenderFns + vm._renderProxy.$options.staticRenderFns = compiledResult.staticRenderFns + const vnodes = compiledResult.render.call( + vm._renderProxy, vm.$createElement + ).children + vm._renderProxy.$options.staticRenderFns = _staticRenderFns + return vnodes + } + return [vm.$createElement(slotValue)] +} + +export default function createRenderSlot ( + options: Object +): ( + name: string, + fallback: ?Array, + props: ?Object, + bindObject: ?Object +) => ?Array { + return function renderSlot ( + name: string, + fallback: ?Array, + props: ?Object, + bindObject: ?Object + ): ?Array { + if (options.slots && options.slots[name]) { + this.$slots[name] = [] + const slotsValue = options.slots[name] + if (Array.isArray(slotsValue)) { + slotsValue.forEach((value) => { + if (typeof value === 'string') { + const vnodes = createVNodes(this, value) + if (Array.isArray(vnodes)) { + this.$slots[name].push(...vnodes) + } + } else { + this.$slots[name].push(this.$createElement(value)) + } + }) + } else { + const vnodes = createVNodes(this, slotsValue) + if (Array.isArray(vnodes)) { + this.$slots[name] = vnodes + } + } + } + return _renderSlot.call(this, name, fallback, props, bindObject) + } +} diff --git a/packages/create-instance/create-scoped-slots.js b/packages/create-instance/create-scoped-slots.js index 6211fd320..46841a04b 100644 --- a/packages/create-instance/create-scoped-slots.js +++ b/packages/create-instance/create-scoped-slots.js @@ -3,6 +3,7 @@ import Vue from 'vue' import { compileToFunctions } from 'vue-template-compiler' import { throwError, vueVersion } from 'shared/util' +import { checkCompileToFunctions } from 'shared/validators' function isDestructuringSlotScope (slotScope: string): boolean { return slotScope[0] === '{' && slotScope[slotScope.length - 1] === '}' @@ -36,6 +37,7 @@ function getVueTemplateCompilerHelpers (): { [name: string]: Function } { } function validateEnvironment (): void { + checkCompileToFunctions() if (window.navigator.userAgent.match(/PhantomJS/i)) { throwError( `the scopedSlots option does not support PhantomJS. ` + diff --git a/packages/create-instance/add-slots.js b/packages/create-instance/create-slot-vnodes.js similarity index 91% rename from packages/create-instance/add-slots.js rename to packages/create-instance/create-slot-vnodes.js index 86f430f82..d9d0c3570 100644 --- a/packages/create-instance/add-slots.js +++ b/packages/create-instance/create-slot-vnodes.js @@ -1,6 +1,7 @@ // @flow import { compileToFunctions } from 'vue-template-compiler' +import { checkCompileToFunctions } from 'shared/validators' function startsWithTag (str: SlotValue): boolean { return typeof str === 'string' && str.trim()[0] === '<' @@ -15,6 +16,7 @@ function createVNodesForSlot ( return slotValue } + checkCompileToFunctions() const el = typeof slotValue === 'string' ? compileToFunctions(slotValue) : slotValue diff --git a/packages/create-instance/index.js b/packages/create-instance/index.js new file mode 100644 index 000000000..f03af3c0a --- /dev/null +++ b/packages/create-instance/index.js @@ -0,0 +1,6 @@ +// @flow + +import createInstance from './create-instance' +import createRenderSlot from './create-render-slot' + +export { createInstance, createRenderSlot } diff --git a/packages/create-instance/package.json b/packages/create-instance/package.json index a9cecc559..c15e81695 100644 --- a/packages/create-instance/package.json +++ b/packages/create-instance/package.json @@ -1,6 +1,6 @@ { "name": "create-instance", "version": "1.0.0-beta.20", - "main": "create-instance.js", + "main": "index.js", "private": true } diff --git a/packages/create-instance/validate-slots.js b/packages/create-instance/validate-slots.js index 97d70a99f..1e4e4bc18 100644 --- a/packages/create-instance/validate-slots.js +++ b/packages/create-instance/validate-slots.js @@ -1,8 +1,10 @@ // @flow import { throwError } from 'shared/util' -import { compileToFunctions } from 'vue-template-compiler' -import { isVueComponent } from '../shared/validators' +import { + checkCompileToFunctions, + isVueComponent +} from 'shared/validators' function isValidSlot (slot: any): boolean { return ( @@ -11,16 +13,6 @@ function isValidSlot (slot: any): boolean { ) } -function requiresTemplateCompiler (slot: any): void { - if (typeof slot === 'string' && !compileToFunctions) { - throwError( - `vueTemplateCompiler is undefined, you must pass ` + - `precompiled components if vue-template-compiler is ` + - `undefined` - ) - } -} - export function validateSlots (slots: SlotsObject): void { Object.keys(slots).forEach(key => { const slot = Array.isArray(slots[key]) ? slots[key] : [slots[key]] @@ -32,7 +24,9 @@ export function validateSlots (slots: SlotsObject): void { `of Components` ) } - requiresTemplateCompiler(slotValue) + if (typeof slotValue === 'string') { + checkCompileToFunctions() + } }) }) } diff --git a/packages/server-test-utils/src/renderToString.js b/packages/server-test-utils/src/renderToString.js index cfdd8ab7e..55a5f4b25 100644 --- a/packages/server-test-utils/src/renderToString.js +++ b/packages/server-test-utils/src/renderToString.js @@ -1,7 +1,7 @@ // @flow import Vue from 'vue' -import createInstance from 'create-instance' +import { createInstance, createRenderSlot } from 'create-instance' import { throwError } from 'shared/util' import { createRenderer } from 'vue-server-renderer' import testUtils from '@vue/test-utils' @@ -29,6 +29,8 @@ export default function renderToString ( throwError(`you cannot use attachToDocument with ` + `renderToString`) } const vueConstructor = testUtils.createLocalVue(options.localVue) + vueConstructor.prototype._t = createRenderSlot(options) + const vm = createInstance( component, mergeOptions(options, config), diff --git a/packages/shared/compile-template.js b/packages/shared/compile-template.js index c19078238..8987d9c89 100644 --- a/packages/shared/compile-template.js +++ b/packages/shared/compile-template.js @@ -1,9 +1,11 @@ // @flow import { compileToFunctions } from 'vue-template-compiler' +import { checkCompileToFunctions } from './validators' export function compileTemplate (component: Component): void { if (component.template) { + checkCompileToFunctions() Object.assign(component, compileToFunctions(component.template)) } diff --git a/packages/shared/stub-components.js b/packages/shared/stub-components.js index c56412de6..26996a9c3 100644 --- a/packages/shared/stub-components.js +++ b/packages/shared/stub-components.js @@ -9,6 +9,7 @@ import { hyphenate } from './util' import { + checkCompileToFunctions, componentNeedsCompiling, templateContainsComponent, isVueComponent @@ -66,13 +67,7 @@ function createStubFromString ( originalComponent: Component, name: string ): Component { - if (!compileToFunctions) { - throwError( - `vueTemplateCompiler is undefined, you must pass ` + - `precompiled components if vue-template-compiler is ` + - `undefined` - ) - } + checkCompileToFunctions() if (templateContainsComponent(templateString, name)) { throwError('options.stub cannot contain a circular reference') @@ -176,13 +171,7 @@ export function createComponentStubs ( } } else { if (typeof stub === 'string') { - if (!compileToFunctions) { - throwError( - `vueTemplateCompiler is undefined, you must pass ` + - `precompiled components if vue-template-compiler is ` + - `undefined` - ) - } + checkCompileToFunctions() components[stubName] = { ...compileToFunctions(stub) } diff --git a/packages/shared/validators.js b/packages/shared/validators.js index 2ba725dec..286615b3f 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -1,6 +1,17 @@ // @flow +import { compileToFunctions } from 'vue-template-compiler' import { throwError, capitalize, camelize, hyphenate } from './util' +export function checkCompileToFunctions (): void { + if (!compileToFunctions) { + throwError( + `vueTemplateCompiler is undefined, you must pass ` + + `precompiled components if vue-template-compiler is ` + + `undefined` + ) + } +} + export function isDomSelector (selector: any): boolean { if (typeof selector !== 'string') { return false diff --git a/packages/test-utils/src/mount.js b/packages/test-utils/src/mount.js index 52638d248..99db619da 100644 --- a/packages/test-utils/src/mount.js +++ b/packages/test-utils/src/mount.js @@ -4,7 +4,6 @@ import './matches-polyfill' import './object-assign-polyfill' import Vue from 'vue' import VueWrapper from './vue-wrapper' -import createInstance from 'create-instance' import createElement from './create-element' import createLocalVue from './create-local-vue' import errorHandler from './error-handler' @@ -12,6 +11,7 @@ import { findAllVueComponentsFromVm } from './find-vue-components' import { mergeOptions } from 'shared/merge-options' import config from './config' import warnIfNoWindow from './warn-if-no-window' +import { createInstance, createRenderSlot } from 'create-instance' Vue.config.productionTip = false Vue.config.devtools = false @@ -28,6 +28,7 @@ export default function mount ( // Remove cached constructor delete component._Ctor const vueConstructor = createLocalVue(options.localVue) + vueConstructor.prototype._t = createRenderSlot(options) const elm = options.attachToDocument ? createElement() : undefined 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..a73b91c25 --- /dev/null +++ b/test/resources/components/component-with-parent-name.vue @@ -0,0 +1,18 @@ + + + diff --git a/test/specs/mounting-options/scopedSlots.spec.js b/test/specs/mounting-options/scopedSlots.spec.js index 05259800d..d89ecc404 100644 --- a/test/specs/mounting-options/scopedSlots.spec.js +++ b/test/specs/mounting-options/scopedSlots.spec.js @@ -4,7 +4,7 @@ import { isRunningPhantomJS } from '~resources/utils' import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue' -import { itDoNotRunIf } from 'conditional-specs' +import { itSkipIf, itDoNotRunIf } from 'conditional-specs' describeWithShallowAndMount('scopedSlots', mountingMethod => { const windowSave = window @@ -165,4 +165,43 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { .with.property('message', message) } ) + + itSkipIf( + mountingMethod.name === 'renderToString', + 'throws error if passed string in default slot object and vue-template-compiler is undefined', + () => { + const compilerSave = + require.cache[require.resolve('vue-template-compiler')].exports + .compileToFunctions + require.cache[ + require.resolve('vue-template-compiler') + ].exports.compileToFunctions = undefined + delete require.cache[require.resolve('../../../packages/test-utils')] + const mountingMethodFresh = require('../../../packages/test-utils')[ + mountingMethod.name + ] + const message = + '[vue-test-utils]: vueTemplateCompiler is undefined, you must pass precompiled components if vue-template-compiler is undefined' + const fn = () => { + mountingMethodFresh(ComponentWithScopedSlots, { + scopedSlots: { + list: '

{{foo.index}},{{foo.text}}

' + } + }) + } + try { + expect(fn) + .to.throw() + .with.property('message', message) + } catch (err) { + require.cache[ + require.resolve('vue-template-compiler') + ].exports.compileToFunctions = compilerSave + throw err + } + require.cache[ + require.resolve('vue-template-compiler') + ].exports.compileToFunctions = compilerSave + } + ) }) diff --git a/test/specs/mounting-options/slots.spec.js b/test/specs/mounting-options/slots.spec.js index 19e338b05..317e6bd53 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', () => { @@ -224,14 +226,18 @@ describeWithMountingMethods('options.slots', mountingMethod => { it('mounts component with text slot', () => { const wrapper = mountingMethod(ComponentWithSlots, { slots: { - default: 'hello,', - header: 'world' + header: 'hello,', + default: 'world' } }) if (mountingMethod.name === 'renderToString') { - expect(wrapper).contains('hello,world') + expect(wrapper).contains( + '
hello,
world
' + ) } else { - expect(wrapper.text()).to.contain('hello,world') + expect(wrapper.html()).to.equal( + '
hello,
world
' + ) } }) @@ -568,4 +574,72 @@ describeWithMountingMethods('options.slots', mountingMethod => { expect(wrapper.contains(ComponentAsAClass)).to.equal(true) } }) + + itDoNotRunIf( + mountingMethod.name === 'renderToString', + 'mounts component with default slot if passed string in slot object', + () => { + const wrapper1 = mount(ComponentWithSlots, { slots: { default: 'foo123{{ foo }}' }}) + expect(wrapper1.find('main').html()).to.equal('
foo123bar
') + const wrapper2 = mount(ComponentWithSlots, { slots: { default: '

1

{{ foo }}2' }}) + expect(wrapper2.find('main').html()).to.equal('

1

bar2
') + const wrapper3 = mount(ComponentWithSlots, { slots: { default: '

1

{{ foo }}

2

' }}) + expect(wrapper3.find('main').html()).to.equal('

1

bar

2

') + const wrapper4 = mount(ComponentWithSlots, { slots: { default: '123' }}) + expect(wrapper4.find('main').html()).to.equal('
123
') + const wrapper5 = mount(ComponentWithSlots, { slots: { default: '1{{ foo }}2' }}) + expect(wrapper5.find('main').html()).to.equal('
1bar2
') + wrapper5.trigger('keydown') + expect(wrapper5.find('main').html()).to.equal('
1BAR2
') + const wrapper6 = mount(ComponentWithSlots, { slots: { default: '

1

2

' }}) + expect(wrapper6.find('main').html()).to.equal('

1

2

') + const wrapper7 = mount(ComponentWithSlots, { slots: { default: '1

2

3' }}) + expect(wrapper7.find('main').html()).to.equal('
1

2

3
') + const wrapper8 = mountingMethod(ComponentWithSlots, { slots: { default: ' space ' }}) + expect(wrapper8.find('main').html()).to.equal('
space
') + } + ) + + itDoNotRunIf( + mountingMethod.name === 'renderToString', + 'sets a component which can access the parent component and the child component', + () => { + const localVue = createLocalVue() + localVue.prototype.bar = 'FOO' + const ParentComponent = mount( + { + name: 'parentComponent', + template: '
', + data () { + return { + time: 1, + childComponentName: '' + } + } + }, + { + components: { + ComponentWithParentName + }, + slots: { + default: [ + '', + '' + ] + }, + localVue + } + ) + const childComponentName = 'component-with-parent-name' + 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('
1,FOO,quux
1,FOO,quux
') + ParentComponent.vm.time = 2 + 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('
2,FOO,quux
2,FOO,quux
') + } + ) }) diff --git a/test/specs/wrapper.spec.js b/test/specs/wrapper.spec.js index 82848ab74..2369d68b5 100644 --- a/test/specs/wrapper.spec.js +++ b/test/specs/wrapper.spec.js @@ -1,4 +1,5 @@ import { describeWithShallowAndMount } from '~resources/utils' +import { itSkipIf } from 'conditional-specs' describeWithShallowAndMount('Wrapper', mountingMethod => { ['vnode', 'element', 'vm', 'options'].forEach(property => { @@ -12,4 +13,41 @@ describeWithShallowAndMount('Wrapper', mountingMethod => { .with.property('message', message) }) }) + + itSkipIf( + mountingMethod.name === 'renderToString', + 'throws error if passed string in default slot array when vue-template-compiler is undefined', + () => { + const compilerSave = + require.cache[require.resolve('vue-template-compiler')].exports + .compileToFunctions + require.cache[require.resolve('vue-template-compiler')].exports = { + compileToFunctions: undefined + } + delete require.cache[require.resolve('../../packages/test-utils')] + const mountingMethodFresh = require('../../packages/test-utils')[ + mountingMethod.name + ] + const message = + '[vue-test-utils]: vueTemplateCompiler is undefined, you must pass precompiled components if vue-template-compiler is undefined' + const fn = () => { + mountingMethodFresh({ + template: '
' + }) + } + try { + expect(fn) + .to.throw() + .with.property('message', message) + } catch (err) { + require.cache[ + require.resolve('vue-template-compiler') + ].exports.compileToFunctions = compilerSave + throw err + } + require.cache[ + require.resolve('vue-template-compiler') + ].exports.compileToFunctions = compilerSave + } + ) })