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,
|
||||
inject,
|
||||
nextTick,
|
||||
provide,
|
||||
ref,
|
||||
render,
|
||||
renderSlot,
|
||||
|
@ -1032,4 +1033,88 @@ describe('defineCustomElement', () => {
|
|||
).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 _numberProps: Record<string, true> | null = null
|
||||
private _styleChildren = new WeakSet()
|
||||
private _pendingResolve: Promise<void> | undefined
|
||||
private _parent: VueElement | undefined
|
||||
/**
|
||||
* dev only
|
||||
*/
|
||||
|
@ -257,15 +259,42 @@ export class VueElement
|
|||
this._parseSlots()
|
||||
}
|
||||
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._resolved) {
|
||||
this._setParent()
|
||||
this._update()
|
||||
} else {
|
||||
this._resolveDef()
|
||||
if (parent && parent._pendingResolve) {
|
||||
this._pendingResolve = parent._pendingResolve.then(() => {
|
||||
this._pendingResolve = undefined
|
||||
this._resolveDef()
|
||||
})
|
||||
} else {
|
||||
this._resolveDef()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _setParent(parent = this._parent) {
|
||||
if (parent) {
|
||||
this._instance!.parent = parent._instance
|
||||
this._instance!.provides = parent._instance!.provides
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._connected = false
|
||||
nextTick(() => {
|
||||
|
@ -285,7 +314,9 @@ export class VueElement
|
|||
* resolve inner component definition (handle possible async component)
|
||||
*/
|
||||
private _resolveDef() {
|
||||
this._resolved = true
|
||||
if (this._pendingResolve) {
|
||||
return
|
||||
}
|
||||
|
||||
// set initial attrs
|
||||
for (let i = 0; i < this.attributes.length; i++) {
|
||||
|
@ -302,6 +333,9 @@ export class VueElement
|
|||
this._ob.observe(this, { attributes: true })
|
||||
|
||||
const resolve = (def: InnerComponentDef, isAsync = false) => {
|
||||
this._resolved = true
|
||||
this._pendingResolve = undefined
|
||||
|
||||
const { props, styles } = def
|
||||
|
||||
// cast Number-type props set before resolve
|
||||
|
@ -346,7 +380,9 @@ export class VueElement
|
|||
|
||||
const asyncDef = (this._def as ComponentOptions).__asyncLoader
|
||||
if (asyncDef) {
|
||||
asyncDef().then(def => resolve((this._def = def), true))
|
||||
this._pendingResolve = asyncDef().then(def =>
|
||||
resolve((this._def = def), true),
|
||||
)
|
||||
} else {
|
||||
resolve(this._def)
|
||||
}
|
||||
|
@ -486,18 +522,7 @@ export class VueElement
|
|||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
instance.parent = parent._instance
|
||||
instance.provides = parent._instance!.provides
|
||||
break
|
||||
}
|
||||
}
|
||||
this._setParent()
|
||||
}
|
||||
}
|
||||
return vnode
|
||||
|
|
Loading…
Reference in New Issue