Skip to content

Commit 71f9ff5

Browse files
authored
fix(custom-element): preserve nested async mount order (#15154)
Closes #15153
1 parent a7513a1 commit 71f9ff5

2 files changed

Lines changed: 117 additions & 2 deletions

File tree

packages/runtime-dom/__tests__/customElement.spec.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,6 +2210,118 @@ describe('defineCustomElement', () => {
22102210
expect(barVal).toBe('bar')
22112211
})
22122212

2213+
test('nested async custom elements should mount ancestor-first', async () => {
2214+
const mounted: string[] = []
2215+
const OuterComp = defineComponent({
2216+
setup() {
2217+
provide('foo', 'foo')
2218+
},
2219+
mounted() {
2220+
mounted.push('outer')
2221+
},
2222+
render() {
2223+
return h('slot')
2224+
},
2225+
})
2226+
const MiddleComp = defineComponent({
2227+
setup() {
2228+
provide('bar', 'bar')
2229+
},
2230+
mounted() {
2231+
mounted.push('middle')
2232+
},
2233+
render() {
2234+
return h('slot')
2235+
},
2236+
})
2237+
const InnerComp = defineComponent({
2238+
setup() {
2239+
const foo = inject('foo')
2240+
const bar = inject('bar')
2241+
return () => h('div', `${foo}/${bar}`)
2242+
},
2243+
mounted() {
2244+
mounted.push('inner')
2245+
},
2246+
})
2247+
let resolveOuter!: (comp: typeof OuterComp) => void
2248+
let resolveMiddle!: (comp: typeof MiddleComp) => void
2249+
let resolveInner!: (comp: typeof InnerComp) => void
2250+
const outerReady = new Promise<typeof OuterComp>(resolve => {
2251+
resolveOuter = resolve
2252+
})
2253+
const middleReady = new Promise<typeof MiddleComp>(resolve => {
2254+
resolveMiddle = resolve
2255+
})
2256+
const innerReady = new Promise<typeof InnerComp>(resolve => {
2257+
resolveInner = resolve
2258+
})
2259+
2260+
const Outer = defineCustomElement(defineAsyncComponent(() => outerReady))
2261+
const Middle = defineCustomElement(defineAsyncComponent(() => middleReady))
2262+
const Inner = defineCustomElement(defineAsyncComponent(() => innerReady))
2263+
2264+
customElements.define('async-chain-outer', Outer)
2265+
customElements.define('async-chain-middle', Middle)
2266+
customElements.define('async-chain-inner', Inner)
2267+
container.innerHTML =
2268+
`<async-chain-outer>` +
2269+
`<async-chain-middle>` +
2270+
`<async-chain-inner></async-chain-inner>` +
2271+
`</async-chain-middle>` +
2272+
`</async-chain-outer>`
2273+
2274+
resolveInner(InnerComp)
2275+
resolveOuter(OuterComp)
2276+
await new Promise(resolve => setTimeout(resolve))
2277+
resolveMiddle(MiddleComp)
2278+
await new Promise(resolve => setTimeout(resolve))
2279+
2280+
const inner = container.querySelector('async-chain-inner') as VueElement
2281+
expect(mounted).toEqual(['outer', 'middle', 'inner'])
2282+
expect(inner.shadowRoot!.innerHTML).toBe('<div>foo/bar</div>')
2283+
})
2284+
2285+
test('should not resolve nested custom element after disconnect', async () => {
2286+
const mounted = vi.fn()
2287+
const ParentComp = defineComponent({
2288+
render() {
2289+
return h('slot')
2290+
},
2291+
})
2292+
let resolveParent!: (comp: typeof ParentComp) => void
2293+
const parentReady = new Promise<typeof ParentComp>(resolve => {
2294+
resolveParent = resolve
2295+
})
2296+
const Parent = defineCustomElement(defineAsyncComponent(() => parentReady))
2297+
const Child = defineCustomElement({
2298+
mounted,
2299+
render() {
2300+
return h('div', 'child')
2301+
},
2302+
})
2303+
2304+
customElements.define('async-disconnect-parent', Parent)
2305+
customElements.define('async-disconnect-child', Child)
2306+
container.innerHTML =
2307+
`<async-disconnect-parent>` +
2308+
`<async-disconnect-child></async-disconnect-child>` +
2309+
`</async-disconnect-parent>`
2310+
2311+
const parent = container.firstChild as VueElement
2312+
const child = parent.firstChild as VueElement
2313+
child.remove()
2314+
resolveParent(ParentComp)
2315+
await new Promise(resolve => setTimeout(resolve))
2316+
2317+
expect(mounted).not.toHaveBeenCalled()
2318+
expect(child.shadowRoot!.innerHTML).toBe('')
2319+
2320+
parent.appendChild(child)
2321+
expect(mounted).toHaveBeenCalledOnce()
2322+
expect(child.shadowRoot!.innerHTML).toBe('<div>child</div>')
2323+
})
2324+
22132325
describe('configureApp', () => {
22142326
test('should work', () => {
22152327
const E = defineCustomElement(

packages/runtime-dom/src/apiCustomElement.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@ export class VueElement
313313
if (parent && parent._pendingResolve) {
314314
this._pendingResolve = parent._pendingResolve.then(() => {
315315
this._pendingResolve = undefined
316-
this._resolveDef()
316+
if (this.isConnected) {
317+
return this._resolveDef()
318+
}
317319
})
318320
} else {
319321
this._resolveDef()
@@ -371,7 +373,7 @@ export class VueElement
371373
*/
372374
private _resolveDef() {
373375
if (this._pendingResolve) {
374-
return
376+
return this._pendingResolve
375377
}
376378

377379
// set initial attrs
@@ -428,6 +430,7 @@ export class VueElement
428430
def.configureApp = this._def.configureApp
429431
resolve((this._def = def), true)
430432
})
433+
return this._pendingResolve
431434
} else {
432435
resolve(this._def)
433436
}

0 commit comments

Comments
 (0)