Skip to content

Commit

Permalink
fix(custom-element): delay mounting of custom elements with async parent
Browse files Browse the repository at this point in the history
close #8127
close #9341
close #9351

the fix is based on #9351 with reused tests
  • Loading branch information
yyx990803 committed Aug 6, 2024
1 parent 03a9ea2 commit 37ccb9b
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 15 deletions.
85 changes: 85 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
h,
inject,
nextTick,
provide,
ref,
render,
renderSlot,
Expand Down Expand Up @@ -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')
})
})
55 changes: 40 additions & 15 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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++) {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 37ccb9b

Please sign in to comment.