diff --git a/flow/options.flow.js b/flow/options.flow.js index 92f1a7231..992cf3d8b 100644 --- a/flow/options.flow.js +++ b/flow/options.flow.js @@ -1,6 +1,7 @@ declare type Options = { // eslint-disable-line no-undef attachToDocument?: boolean, + attachTo?: HTMLElement | string, propsData?: Object, mocks?: Object, methods?: { [key: string]: Function }, @@ -17,6 +18,7 @@ declare type Options = { } declare type NormalizedOptions = { + attachTo?: HTMLElement | string, attachToDocument?: boolean, propsData?: Object, mocks: Object, diff --git a/packages/shared/validate-options.js b/packages/shared/validate-options.js index 1ad9c90ac..452d64eb4 100644 --- a/packages/shared/validate-options.js +++ b/packages/shared/validate-options.js @@ -1,11 +1,13 @@ import { isPlainObject, isFunctionalComponent, - isConstructor + isConstructor, + isDomSelector, + isHTMLElement } from './validators' import { VUE_VERSION } from './consts' import { compileTemplateForSlots } from './compile-template' -import { throwError } from './util' +import { throwError, warn } from './util' import { validateSlots } from './validate-slots' function vueExtendUnsupportedOption(option) { @@ -22,6 +24,20 @@ function vueExtendUnsupportedOption(option) { const UNSUPPORTED_VERSION_OPTIONS = ['mocks', 'stubs', 'localVue'] export function validateOptions(options, component) { + if ( + options.attachTo && + !isHTMLElement(options.attachTo) && + !isDomSelector(options.attachTo) + ) { + throwError( + `options.attachTo should be a valid HTMLElement or CSS selector string` + ) + } + if ('attachToDocument' in options) { + warn( + `options.attachToDocument is deprecated in favor of options.attachTo and will be removed in a future release` + ) + } if (options.parentComponent && !isPlainObject(options.parentComponent)) { throwError( `options.parentComponent should be a valid Vue component options object` diff --git a/packages/shared/validators.js b/packages/shared/validators.js index 09fcb384b..55da960d6 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -112,6 +112,14 @@ export function isPlainObject(c: any): boolean { return Object.prototype.toString.call(c) === '[object Object]' } +export function isHTMLElement(c: any): boolean { + if (typeof HTMLElement === 'undefined') { + return false + } + // eslint-disable-next-line no-undef + return c instanceof HTMLElement +} + export function isRequiredComponent(name: string): boolean { return ( name === 'KeepAlive' || name === 'Transition' || name === 'TransitionGroup' diff --git a/packages/test-utils/src/mount.js b/packages/test-utils/src/mount.js index c6dab106d..0a0ef4128 100644 --- a/packages/test-utils/src/mount.js +++ b/packages/test-utils/src/mount.js @@ -28,7 +28,8 @@ export default function mount(component, options = {}) { const parentVm = createInstance(component, mergedOptions, _Vue) - const el = options.attachToDocument ? createElement() : undefined + const el = + options.attachTo || (options.attachToDocument ? createElement() : undefined) const vm = parentVm.$mount(el) component._Ctor = {} @@ -36,7 +37,7 @@ export default function mount(component, options = {}) { throwIfInstancesThrew(vm) const wrapperOptions = { - attachedToDocument: !!mergedOptions.attachToDocument + attachedToDocument: !!el } const root = parentVm.$options._isFunctionalContainer diff --git a/test/specs/mount.spec.js b/test/specs/mount.spec.js index 279f9224a..d9869c512 100644 --- a/test/specs/mount.spec.js +++ b/test/specs/mount.spec.js @@ -293,6 +293,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { expect(wrapper.vm.$options.context).to.equal(undefined) expect(wrapper.vm.$options.attrs).to.equal(undefined) expect(wrapper.vm.$options.listeners).to.equal(undefined) + wrapper.destroy() }) itDoNotRunIf(vueVersion < 2.3, 'injects store correctly', () => { @@ -366,6 +367,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { '

\n' + '' ) + wrapper.destroy() }) it('overwrites the component options with the instance options', () => { diff --git a/test/specs/mounting-options/attachTo.spec.js b/test/specs/mounting-options/attachTo.spec.js new file mode 100644 index 000000000..2fba92b14 --- /dev/null +++ b/test/specs/mounting-options/attachTo.spec.js @@ -0,0 +1,62 @@ +import { describeWithShallowAndMount } from '~resources/utils' + +const innerHTML = 'Hello world' +const outerHTML = `
${innerHTML}
` +const ssrHTML = `
${innerHTML}
` +const template = '
Hello world
' +const TestComponent = { template } + +describeWithShallowAndMount('options.attachTo', mountingMethod => { + it('attaches to a provided HTMLElement', () => { + const div = document.createElement('div') + div.id = 'root' + document.body.appendChild(div) + expect(document.getElementById('root')).to.not.be.null + expect(document.getElementById('attach-to')).to.be.null + const wrapper = mountingMethod(TestComponent, { + attachTo: div + }) + expect(document.getElementById('root')).to.be.null + expect(document.getElementById('attach-to')).to.not.be.null + expect(document.getElementById('attach-to').outerHTML).to.equal(outerHTML) + expect(wrapper.options.attachedToDocument).to.equal(true) + wrapper.destroy() + expect(document.getElementById('attach-to')).to.be.null + }) + it('attaches to a provided CSS selector string', () => { + const div = document.createElement('div') + div.id = 'root' + document.body.appendChild(div) + expect(document.getElementById('root')).to.not.be.null + expect(document.getElementById('attach-to')).to.be.null + const wrapper = mountingMethod(TestComponent, { + attachTo: '#root' + }) + expect(document.getElementById('root')).to.be.null + expect(document.getElementById('attach-to')).to.not.be.null + expect(document.getElementById('attach-to').outerHTML).to.equal(outerHTML) + expect(wrapper.options.attachedToDocument).to.equal(true) + wrapper.destroy() + expect(document.getElementById('attach-to')).to.be.null + }) + + it('correctly hydrates markup', () => { + expect(document.getElementById('attach-to')).to.be.null + + const div = document.createElement('div') + div.id = 'attach-to' + div.setAttribute('data-server-rendered', 'true') + div.innerHTML = innerHTML + document.body.appendChild(div) + expect(div.outerHTML).to.equal(ssrHTML) + const wrapper = mountingMethod(TestComponent, { + attachTo: '#attach-to' + }) + const rendered = document.getElementById('attach-to') + expect(rendered).to.not.be.null + expect(rendered.outerHTML).to.equal(outerHTML) + expect(wrapper.options.attachedToDocument).to.equal(true) + wrapper.destroy() + expect(document.getElementById('attach-to')).to.be.null + }) +})