mirror of https://github.com/vuejs/core.git
fix(custom-element): delay mounting of custom elements with async parent
close #8127 close #9341 close #9351 the fix is based on #9351 with reused tests
This commit is contained in:
parent
03a9ea2b88
commit
37ccb9b9a0
|
@ -10,6 +10,7 @@ import {
|
||||||
h,
|
h,
|
||||||
inject,
|
inject,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
provide,
|
||||||
ref,
|
ref,
|
||||||
render,
|
render,
|
||||||
renderSlot,
|
renderSlot,
|
||||||
|
@ -1032,4 +1033,88 @@ describe('defineCustomElement', () => {
|
||||||
).toHaveBeenWarned()
|
).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('async & nested custom elements', async () => {
|
||||||
|
let fooVal: string | undefined = ''
|
||||||
|
const E = defineCustomElement(
|
||||||
|
defineAsyncComponent(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
setup(props) {
|
||||||
|
provide('foo', 'foo')
|
||||||
|
},
|
||||||
|
render(this: any) {
|
||||||
|
return h('div', null, [renderSlot(this.$slots, 'default')])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const EChild = defineCustomElement({
|
||||||
|
setup(props) {
|
||||||
|
fooVal = inject('foo')
|
||||||
|
},
|
||||||
|
render(this: any) {
|
||||||
|
return h('div', null, 'child')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
customElements.define('my-el-async-nested-ce', E)
|
||||||
|
customElements.define('slotted-child', EChild)
|
||||||
|
container.innerHTML = `<my-el-async-nested-ce><div><slotted-child></slotted-child></div></my-el-async-nested-ce>`
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r))
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
|
||||||
|
expect(fooVal).toBe('foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('async & multiple levels of nested custom elements', async () => {
|
||||||
|
let fooVal: string | undefined = ''
|
||||||
|
let barVal: string | undefined = ''
|
||||||
|
const E = defineCustomElement(
|
||||||
|
defineAsyncComponent(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
setup(props) {
|
||||||
|
provide('foo', 'foo')
|
||||||
|
},
|
||||||
|
render(this: any) {
|
||||||
|
return h('div', null, [renderSlot(this.$slots, 'default')])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const EChild = defineCustomElement({
|
||||||
|
setup(props) {
|
||||||
|
provide('bar', 'bar')
|
||||||
|
},
|
||||||
|
render(this: any) {
|
||||||
|
return h('div', null, [renderSlot(this.$slots, 'default')])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const EChild2 = defineCustomElement({
|
||||||
|
setup(props) {
|
||||||
|
fooVal = inject('foo')
|
||||||
|
barVal = inject('bar')
|
||||||
|
},
|
||||||
|
render(this: any) {
|
||||||
|
return h('div', null, 'child')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
customElements.define('my-el-async-nested-m-ce', E)
|
||||||
|
customElements.define('slotted-child-m', EChild)
|
||||||
|
customElements.define('slotted-child2-m', EChild2)
|
||||||
|
container.innerHTML =
|
||||||
|
`<my-el-async-nested-m-ce>` +
|
||||||
|
`<div><slotted-child-m>` +
|
||||||
|
`<slotted-child2-m></slotted-child2-m>` +
|
||||||
|
`</slotted-child-m></div>` +
|
||||||
|
`</my-el-async-nested-m-ce>`
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r))
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
|
||||||
|
expect(fooVal).toBe('foo')
|
||||||
|
expect(barVal).toBe('bar')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -207,6 +207,8 @@ export class VueElement
|
||||||
private _resolved = false
|
private _resolved = false
|
||||||
private _numberProps: Record<string, true> | null = null
|
private _numberProps: Record<string, true> | null = null
|
||||||
private _styleChildren = new WeakSet()
|
private _styleChildren = new WeakSet()
|
||||||
|
private _pendingResolve: Promise<void> | undefined
|
||||||
|
private _parent: VueElement | undefined
|
||||||
/**
|
/**
|
||||||
* dev only
|
* dev only
|
||||||
*/
|
*/
|
||||||
|
@ -257,14 +259,41 @@ export class VueElement
|
||||||
this._parseSlots()
|
this._parseSlots()
|
||||||
}
|
}
|
||||||
this._connected = true
|
this._connected = true
|
||||||
|
|
||||||
|
// locate nearest Vue custom element parent for provide/inject
|
||||||
|
let parent: Node | null = this
|
||||||
|
while (
|
||||||
|
(parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
|
||||||
|
) {
|
||||||
|
if (parent instanceof VueElement) {
|
||||||
|
this._parent = parent
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
if (this._resolved) {
|
if (this._resolved) {
|
||||||
|
this._setParent()
|
||||||
this._update()
|
this._update()
|
||||||
|
} else {
|
||||||
|
if (parent && parent._pendingResolve) {
|
||||||
|
this._pendingResolve = parent._pendingResolve.then(() => {
|
||||||
|
this._pendingResolve = undefined
|
||||||
|
this._resolveDef()
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this._resolveDef()
|
this._resolveDef()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setParent(parent = this._parent) {
|
||||||
|
if (parent) {
|
||||||
|
this._instance!.parent = parent._instance
|
||||||
|
this._instance!.provides = parent._instance!.provides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this._connected = false
|
this._connected = false
|
||||||
|
@ -285,7 +314,9 @@ export class VueElement
|
||||||
* resolve inner component definition (handle possible async component)
|
* resolve inner component definition (handle possible async component)
|
||||||
*/
|
*/
|
||||||
private _resolveDef() {
|
private _resolveDef() {
|
||||||
this._resolved = true
|
if (this._pendingResolve) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// set initial attrs
|
// set initial attrs
|
||||||
for (let i = 0; i < this.attributes.length; i++) {
|
for (let i = 0; i < this.attributes.length; i++) {
|
||||||
|
@ -302,6 +333,9 @@ export class VueElement
|
||||||
this._ob.observe(this, { attributes: true })
|
this._ob.observe(this, { attributes: true })
|
||||||
|
|
||||||
const resolve = (def: InnerComponentDef, isAsync = false) => {
|
const resolve = (def: InnerComponentDef, isAsync = false) => {
|
||||||
|
this._resolved = true
|
||||||
|
this._pendingResolve = undefined
|
||||||
|
|
||||||
const { props, styles } = def
|
const { props, styles } = def
|
||||||
|
|
||||||
// cast Number-type props set before resolve
|
// cast Number-type props set before resolve
|
||||||
|
@ -346,7 +380,9 @@ export class VueElement
|
||||||
|
|
||||||
const asyncDef = (this._def as ComponentOptions).__asyncLoader
|
const asyncDef = (this._def as ComponentOptions).__asyncLoader
|
||||||
if (asyncDef) {
|
if (asyncDef) {
|
||||||
asyncDef().then(def => resolve((this._def = def), true))
|
this._pendingResolve = asyncDef().then(def =>
|
||||||
|
resolve((this._def = def), true),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
resolve(this._def)
|
resolve(this._def)
|
||||||
}
|
}
|
||||||
|
@ -486,18 +522,7 @@ export class VueElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// locate nearest Vue custom element parent for provide/inject
|
this._setParent()
|
||||||
let parent: Node | null = this
|
|
||||||
while (
|
|
||||||
(parent =
|
|
||||||
parent && (parent.parentNode || (parent as ShadowRoot).host))
|
|
||||||
) {
|
|
||||||
if (parent instanceof VueElement) {
|
|
||||||
instance.parent = parent._instance
|
|
||||||
instance.provides = parent._instance!.provides
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vnode
|
return vnode
|
||||||
|
|
Loading…
Reference in New Issue